拉巴力的纸皮箱


  • 首页

  • 标签

  • 归档

  • 关于

  • 搜索

ToB业务随便讲讲

发表于 2024-04-01

以下内容格局比较小,仅做个人记录

  • 从我个人接触过的ToB和ToC业务讲,有以下特点(当然肯定不是一定对的,毕竟业务形态实在太多,我没真正接触过的,就不提了):

    • ToC的业务团队,一般只做若干个业务模块;不同的业务有不同的团队
    • ToB的业务团队,需要对接几乎所有业务,进而对外提供平台能力
  • 关于ToB业务的规范

    • ToB的业务,出于通用性,会制定各种规范由客户进行对接
    • 当然,所谓的规范,有时候是看哪方处于强势
    • 弱势方即使作为平台也要给客户做定制化
    • 比如一个简单的回调规范,客户不配合,需要平台这边按照他们的规范适配
    • 但同样支付宝和微信作为平台,难道要支付宝和微信去适配每个接入方?无非是谁更强势的问题。
    • 从这方面看,可以一定判断出这个公司目前的状况

提升自己职场中的“可替代性”

发表于 2024-03-29

怎么看待职场中的“业务壁垒”?

  • 在我的工作中,曾经发生过这样一件事:有一天我们在讨论,能不能实现一个业务通用组件,让各个业务接入,提升业务需求的开发效率等,这时候有一个同事就跟我说:‘那如果真的实现的话,那你这边的业务就啥都没了’,言外之意就是我这边“没事做了”。我给他的回答是:‘没关系,只要是有意义,有价值,对公司是正确的就行’

  • 那么?我真的那么爱公司吗?

    • 当然不是,我只是觉得应该做自己认为正确的事,顺便说一下漂亮话忽悠一下。
  • 在工作中,你们到遇到很多“业务壁垒”。通俗来讲,这个东西只有他会,或者短期内只有他能搞定。你甚至会觉得一股恶心,就是很乱很难受,并惊叹对方的忍耐力。

  • 然而,无数的事实证明了,这个世界不会因为没有谁就不行。

  • 而我的原则是:做正确的事,不制造业务壁垒;保持出色的整理学习能力,输出业务文档,让自己随时可替换,甚至寻找可以替换自己的人;不做狭义对自己有利的事,尽量做广义对公司有利的事。

  • 具体来说,可以这样阐述

    1. “业务壁垒”不是我认同的核心竞争力,甚至混乱的“业务壁垒”不是我能忍受的工作体验;
    2. 出色的整理学习能力,坚信长期主义,才是我认同的核心竞争力;
    3. 我在提升自己可替代性的同时,也是在提升自己的“不可替代性”;而有些地方,并不需要“不可替代性”的人;
    4. 提升自己可替代性,其实也是在保持业务的稳定;不会因个人而大受影响,甚至我因此可以放心度假,因为我会的东西,别人也可以快速学会和处理;
  • 关于“可替代性”另外补充:

    • 增加自己的安全感(休假可以找到别人)
    • 增加别人(比如上级)的安全感(万一找不到,出意外有其他人解决)
    • 个人清高,一定程度上,喜欢更高维度的不可替代性

对服务架构中的聚合层理解

发表于 2024-03-28

以前刚毕业的时候,进入一个组,叫中间层,那时候还懵懵懂懂不知道想表达啥意思

  • 图来源网上,已未知出处

  • 这个组是直接给app提供接口的,主要的职能大概有以下

    • 业务适配
    • 服务聚合
    • 数据展示
    • 安全隔离
  • 聚合服务,简单的来说就是聚合底层的各个业务系统,给用户端应用提供接口

    • (直接对接用户前端的接口服务,手机app,网站,h5等用户终端)
    • BFF —— Backends for frontends(服务于前端的后端),是为了让后端API满足不同的前端使用场景,而演进出来的一种模式。
    • BFF避坑指南
  • 那么这个组有存在的必要吗?特别是现在服务内部都微服务化,很多业务服务都是直接对app提供接口的。

  • 先从表面上看看使不使用聚合层各自的特点和好处

    • 使用聚合层
      1. 前端需要对接的接口比较少,对前端来说比较友好
      2. 聚合层可以聚合底层业务的接口,相比前端直接对接,有些业务场景可以提升接口性能
      3. 能根据前端的业务场合适配和统一改动,便于快速迭代和打补丁等
      4. 可以根据前端对数据的不同使用场景,减少不必要的性能损耗
        • 如果统一接口,入参就会变得复杂,增加前端的对接成本
    • 不使用聚合层
      1. 有些业务场景,减少多一层调用,有利于提升性能
      2. 业务接口可以复用,不需要聚合层再包一层,减少开发时间
        • 前提:整个公司统一接口规范,同个接口可以复用于不同app
      3. 分散接口故障风险。一个业务故障,不会导致其他业务也故障。
        • 相对使用聚合层,聚合层的服务不可用,整个app都会受影响
  • 仔细想想,简单理解,其实使用聚合层就是水平架构,不使用就是垂直架构;根据康威定律,其实公司决定使用哪种方案,一定程度还受公司组织关系的影响。

  • 提外话:由于前后端分离的流行。聚合层服务有时候会由前端团队来维护。使用node服务用于聚合页面和后端接口,从而提升性能。相对于传统的聚合层,前端的聚合层比较轻量化,基本是无状态服务,不存储数据和加工数据,纯粹做聚合接口和页面。

扩展

  • “胖瘦” BFF:常见的两种微服务形态

职场中“马后炮”现象

发表于 2024-03-27

职场中有一种奇怪又平常的现象:没出事就没人重视,出事了就各种马后炮。

  • 以软件开发人员平常应对的事情为例,这里其实有一种无奈的解释:

    1. 需求这么多还没做完,有什么充分的理由去重视这个问题?
    2. 你把事情做好了,完全不出事,怎么证明是你的功劳?
  • 普通的干实事的程序员,最讨厌跟行外人谈收益。就像把代码写好了,自然后续维护成本就低了。但是硬要把他转化为节约多少人力。只想一心老老实实写代码的程序员,好心做个事情还要花费心力想办法给你解释,久而久之自然是多一事不如少一事。所谓劣币驱逐良币,有追求的程序员自然离开了,剩下的可想而知。

  • 那么,难道这个问题就无解吗?

    • 当然不是。要建立在不干涉和充分信任的基础上。CEO相信CTO,CTO当然知道把代码写好的好处,所以也不需要底下做事的人过分解释。
    • 但实际上很多公司,CTO不做CTO的事,只是专注向上汇报,还把汇报的事情层层外包给下面的人。导致本应该自己屏蔽,让底下安心做事的事情,层层透传,苦了干实事的人。而自己就成了汇总PPT的汇报员。
  • 最后讲一个故事

    魏文王问扁鹊曰:“子昆弟三人其孰最善为医?”
    扁鹊曰:“长兄最善,中兄次之,扁鹊最为下。”
    魏文王曰:“可得闻邪?”
    扁鹊曰:“长兄于病视神,未有形而除之,故名不出于家。中兄治病,其在毫毛,故名不出于闾。若扁鹊者,镵血脉,投毒药,副肌肤,故名出闻于诸侯。”
    魏文王曰:“善。”
    
  • 善战者无赫赫之功

  • 防患于未然的人不如亡羊补牢者获得的赞誉多

小团队管理经验

发表于 2024-03-26

转眼工作十年了,从20年10月开始,断断续续做了近三年的小团队管理,这里作简要总结一下。

所谓小团队管理,其实就是现在说的虚线组长,需要写代码、给团队提供指导等,但是由于不是高级别大领导,不需要应付很多烦人的破事。从这层来看,挺适合我这种性格的人。

以下是以小团队为背景来总结的,格局显得较小;当然每个人的观点不一样,不一定是正确的理解。

核心要点(对自己)

  • 情商、基于别人的角度思考
    • 个人习惯采取对内温和对外强硬的方式;当然结合自身情况和环境具体选择;这个方法可以让你更好的理解人和事情;我不认为所谓的“情商高”就是“老好人”。
  • 以身作则
    • 以自己为榜样,让团队其他成员看到正确的做事方式等;包括技术方案、沟通方式等,当然包括一些无法避免的脏活累活等;用高效和人性化等方式应对 。
  • 令人信服的技术功底
    • 小团队管理的特点,就是领头的人也是需要写代码和干实事,而且跟团队成员的距离是最近的;需要保持自己良好的技术功底,才能更好的给团队提供有效的建议;另外作为有追求的技术人,谁都想跟着比自己优秀的人一起共事。
  • 保证成员尽量免干扰、休息时间如无必要不打扰
    • 这一点是我基于人性化的角度考虑的,有些人可能会觉得不重要。给团队提供深度工作的环境,才能提升效率;在一些不紧急的事情少,要做到不随意打扰,减少团队的负面情绪。对应上面第一条,将心比心,每个人都想要良好的工作体验。
  • 真诚、谦虚、透明
    • 尽量保持团队一定的透明度,减少不必要的暗箱操作。大部分情况下不需要开会,只需要一篇简明扼要的文档,即可让团队快速了解事情的内容。团队成员也在发展,要清楚有一天他们的能力会超过你。而我一直坚持的做法是,业务上坚持写文档,尽量减少业务壁垒(也是为自己放假偷懒准备),技术上提供合理的建议,不遗余力的提高自己的“可替代性”。
    • 认清自己。领导需要团队的力量才能发挥作用。不要错把平台的能力当作自己的能力。把公司的给予当作自己的给予。
  • 人性化管理
    • 现在很多公司都追求狼性文化,说着各种假话哄下属为自己加班干活
    • 个人管理经验理念是,先做人再做领导
    • 如非必要。不严厉。许多人还没到那个层级就采取低端没人情味的管理方式。要和同事成为朋友,营造友好的工作环境,而不是一味向上管理。
  • 尽量保持技术更新升级
    • 在确保充分测试,稳定灰度的同时,适量的升级有利于系统的稳定,增加维护人员的积极性。这种东西无法直接给出具体收益,但应该长远考虑,一味的保守只会让技术越来越落后

核心要点(对团队)

  • 聪明、叙述简明扼要、能抓重点
  • 乐于分享、采用高效的分享手段(文章即可)、实事求是
  • 有团队主人翁意识,主动了解业务
  • 对技术原理好奇并积极探索
  • 尊重别人的时间
  • 跟进事情有反馈和结论
  • 在正确的事情面前,敢于说不
  • 重视自己的“可替代性”、免于个人单点、提升团队容错性

小团队技术管理者的事项列表

  • 业务侧
    • 快速满足需求
    • 通用业务组件抽象
    • 旧业务熟悉
    • 定期删除无用代码,增加代码可维护性
    • 遗留需求记录和跟进
    • 业务知识文档化,方便查阅和备忘
    • 需求上线时间定时跟进(你就是项目经理)
  • 技术侧
    • 旧业务梳理和重构
    • 架构灾备和优化、资源隔离等
    • 通用工具类封装
    • 管理后台搭建 (解放技术)
    • 服务异常监控(后端接口,前端APM等各个维度)
    • 技术组件升级,提升团队技术广度
    • 废弃业务下线,回收资源
    • 业务技术调研,团队具备快速满足产品新需求的能力
    • 分享,相互学习,个人认为文档就行了,会议实际上挺形式和浪费大家的时间
    • 帮助review方案和代码
    • 技术债务登记和逐步处理(分优先级[每个季度一定要完成一定比例的低优先级任务],保持代码洁癖,不给后来人挖坑)
    • 开发规范制定(避免后续维护混乱,也方便大家日常开发查阅)
    • 开发阶段和上线阶段checklist,避免犯重复的错误,避免上线遗漏导致项目延期
    • 技术面试
    • 应急处理、日常维护等文档化(公开、透明、高效)
    • 团队文档整理规划
      • 新人指引
      • 业务模块(服务模块、业务知识整理、业务交接记录、业务安全风险点)
      • 工作规划(需求列表、技术规划、技术债务)
      • 开发规范(项目上线流程、公共库说明、开发协议、服务架构图、业务逻辑开发套路等)
      • 工作文档(应急、日常维护等)
        • 常见问题处理导航
      • 技术文档(技术型、分享类)
      • 技术方案(日常需求上线方案归档)
      • 复盘、故障报告
      • 业务调研
      • 其他备份

Reference

  • 个人管理经验总结

如何在大量数据中判断给定的是否在其中

发表于 2023-07-30

1、 Bloom Filter

  • 缺点:只能增不能减,且有误判场景需要单独处理
  • 可以使用当前进程级的Bloom Filter,也可以使用Redis的Bloom Filter

2、 文件存储

  • 可以使用类似Java的MappedByteBuffer来保存数据

  • 缺点:只能保存连续的一定范围的数字对应的值

  • Java文件读写与MappedByteBuffer

  • 使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import org.junit.Test;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class MappedByteBufferTest {
public static void main(String[] args) {
System.out.println("=="+ ID_SIZE);
System.out.println("=="+ getBufferIndex(1032387525));
System.out.println("=="+ getBufferIndex(227081157));
// 两个数字模结果冲突
// 这种方案只能保存连续的一定范围的数字对应的值
}

private static final int ID_SIZE = 1024 * 1024 * 32 * 8;

private static int getBufferIndex(long idx) {
//每个id占用8个字节
return (int) ((idx % ID_SIZE) * 8);
}

/**
*
* 写入:mappedByteBuffer.putLong(int index, long value);
* 读取:mappedByteBuffer.getLong(int index);
* 注:MappedByteBuffer对index的最大范围是Int.MaxValue()
*/

// 上面的表述计算有问题
/**
* 2G = 2^31 字节
* ==> 1b= 2^3 ; 1k = 2^10 b ; 1M = 2^20 b ; 1G = 2^30 b; 2G = 2^31 字节
* long 64 位,即 2^3 字节 = 2^3
* 2G = 2^31 字节 能存储多少个,应该是 2^31 / 2^3 = 2^28
*/

// 31 - 3 = 28 = 10 + 10 + 5 = 1024 * 1024 * 32 * 8
// 根据每个模之后的倍数放在不通的文件,所以不会冲突
//如果分文件,比如10个,那么78在第8个文件的低第七个; 78%10、78/10

/**
* https://zhuanlan.zhihu.com/p/353988136
* @throws IOException
*/
@Test
public void testMappedByteBuffer() throws IOException {
File file = new File("/Users/kingsonwu/downloads/test.log");
if(!file.exists()){
file.createNewFile();
}
FileChannel fileChannel = FileChannel.open(Paths.get(URI.create("file:/Users/kingsonwu/downloads/test.log")),
StandardOpenOption.WRITE, StandardOpenOption.READ);
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, Integer.MAX_VALUE);
//fileChannel.close();
//mappedByteBuffer.position(1024);
//mappedByteBuffer.putLong(10000L);

mappedByteBuffer.putLong(getBufferIndex(1032387525), 1032387528111L);
mappedByteBuffer.putLong(getBufferIndex(1032387526), 1032387529567L);
mappedByteBuffer.putLong((ID_SIZE - 2)*8, 4032387529569443654L);
//mappedByteBuffer.putLong((ID_SIZE_2)*8, 1032387529569L);

mappedByteBuffer.force();

System.out.println(mappedByteBuffer.getLong(getBufferIndex(1032387525)));
System.out.println(mappedByteBuffer.getLong(getBufferIndex(1032387526)));
System.out.println(mappedByteBuffer.getLong((ID_SIZE - 2)*8));
//System.out.println(mappedByteBuffer.getLong((ID_SIZE_2 )*8));

}
}

3、Tendis

  • 使用tendis可以将数据存储到磁盘,避免全部数据都保存在内存,从而节约内存
  • Redis的痛点之一就是内存成本高,访问频率(QPS)没那么高, 依然占用大量机器, 维护成本很高
  • Redis vs Tendis:冷热混合存储版架构揭秘
  • 使用 rocksdb 作为存储引擎, LSM Tree

记一次socket泄露问题排查记录

发表于 2023-07-30

结论先行

  • 服务死锁导致大量请求hang住,进程无法提供服务且大量socket泄漏

具体过程

  • 服务某些逻辑处理不当导致死锁
  • 大量请求hang住没有响应
  • 上层的nginx超时主动关闭连接
  • 服务被关闭连接,导致大量CLOSE_WAIT
  • 由于服务对连接处理不当(待分析原因TODO),导致大量socket泄漏

前置知识

TCP状态图

  • 从网上找的

相关排查命令

sudo lsof | grep sock | awk '{++S[$1]} END {for(a in S) print a, S[a]}' | sort -nr
sudo lsof | awk '/sock/ {++S[substr($0, index($0, $9))]} END {for(a in S) print a, S[a]}'
sudo lsof | grep sock|grep 'identify protocol'|awk ' {++S[$2]} END {for(a in S) print a, S[a]}'| sort -nr
//打开句柄数目最多的进程
lsof -n|awk '{print $2}'| sort | uniq -c | sort -nr | head
//查看close_wait连接统计:
sudo netstat -anp|grep 'CLOSE_WAIT'|grep 'ssogo'| awk '{print $5}'|sort |uniq -c | sort -nr
//查看FIN_WAIT2连接统计:
sudo netstat -anp|grep 'FIN_WAIT2'|grep 'ssogo'| awk '{print $5}'|sort |uniq -c | sort -nr
//查看CLOSE_WAIT最多的进程
sudo netstat -anp|grep 'CLOSE_WAIT'| awk '{print $7}'|sort |uniq -c | sort -nr
sudo netstat -anp|grep 'FIN_WAIT2'| awk '{print $7}'|sort |uniq -c | sort -nr

ss

  • 在 ss -s 命令的输出中,”closed” 表示已关闭的 TCP 连接的数量。

具体地,TCP 的 “closed” 状态表示连接已经关闭,不再使用,并且不再具有任何网络连接或状态信息。这些连接处于已经结束的状态,但它们的套接字资源还未被完全释放或回收。

在输出结果中,”closed” 数量是指当前处于已关闭状态的 TCP 连接的数量。这些连接可能是先前已经建立的连接,已经完成了其通信任务,双方都关闭了连接,并且连接的套接字资源等待系统进行资源回收。

需要注意的是,已关闭的连接数量的增加和减少是动态的,因为在系统的运行过程中,套接字的打开和关闭是常见的网络操作。

lsof

  • lsof| grep "can't identify protocol"
    如果存在很多,则代表socket泄漏,同时会显示哪个进程使用的sock未关闭。

  • tcp-socket文件句柄泄漏


架构图

  • TODO

处理过程1

  • 紧急重启服务(业务Go服务)后socket数量下降

  • 在现场已经没了的情况下,分析发现机器上本身已经存在大量socket泄漏的情况

![](20230730-记一次socket泄露问题排查记录/identify protocol.png)


  • nginx和gateway都reload,那么nginx会产生新的worker进程,但是应该shutdown的老进程因为和gateway还有连接,所以也不会销毁,这样时间长了会有很多处于shutting状态的进程,这些进程都会占用资源。

  • 初步怀疑是这些异常的nginx worker进程导致的,于是处理所有机器上这些异常(kill)

    1. 如果清掉这些异常进程后,问题不再发生,那么很大可能就是这个原因导致的
    2. 如果问题还继续发生,说明是其他的原因

处理过程2

  • 即使上次清理了所有有问题的nginx worker进程,释放了大量泄漏的socket,相同的问题后续还是发生了

  • 还是出现了大量的closed状态,已经大量的close_wait和fin_wait2状态

  • 通过lsof查看,确实是Go业务进程泄漏的socket

  • 上图的正常状态下,close_wait和fin_wait2状态的数量,并没那么多

  • todo: 后续统计一下:

//查看close_wait连接统计:
sudo netstat -anp|grep 'CLOSE_WAIT'|grep 'ssogo'| awk '{print $5}'|sort |uniq -c | sort -nr
//查看FIN_WAIT2连接统计:
sudo netstat -anp|grep 'FIN_WAIT2'|grep 'ssogo'| awk '{print $5}'|sort |uniq -c | sort -nr
//查看CLOSE_WAIT最多的进程
sudo netstat -anp|grep 'CLOSE_WAIT'| awk '{print $7}'|sort |uniq -c | sort -nr
sudo netstat -anp|grep 'FIN_WAIT2'| awk '{print $7}'|sort |uniq -c | sort -nr
  • 通过pprof输出进程的goroutine情况,从数量和堆栈分析,大量goroutine锁住,导致请求被hang住

  • 至此,问题的基本表现分析如下

    1. Go进程出问题hang住大量请求
    2. 上层的nginx超时主动关闭连接,状态变成FIN_WAIT2,而Go进程对应的socket拦截则变成CLOSE_WAIT
      • 查看设置为 proxy_read_timeout 10(即10s):proxy_read_timeout 是用来设置与后端代理服务器之间的读取超时时间,它控制 Nginx 从后端代理服务器读取响应的最长等待时间。当从后端服务器读取响应数据的时间超过了设置的超时时间,Nginx 将认为后端服务器的响应已经超时,并且会中断与后端服务器的连接。
    3. Go进程对已关闭的连接处理异常,导致大量“can’t identify protocol”
      • 请求被hang住,进程没释放,socket会占有(不然被新的请求占有造成数据包混乱)

扩展

can’t identify protocol 是怎么出现的?

  • https://idea.popcount.org/2012-12-09-lsof-cant-identify-protocol/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import socket
import os
import sys

PORT = 9918

sd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sd.bind(('0.0.0.0', PORT))
sd.listen(5)

for i in range(10):
if os.fork() == 0:
sd.close()
cd = socket.socket(socket.AF_INET,
socket.SOCK_STREAM)
cd.connect(('127.0.0.1', PORT))
sys.exit()

print "Server process pid=%i" % (os.getpid(),)
sockets = []
for i in range(10):
(cd, address) = sd.accept()
sockets.append(cd)
cd.shutdown(socket.SHUT_WR)

os.system("lsof -p %i" % (os.getpid(),))
```

+ `cd.connect(('127.0.0.1', PORT))`之后没有执行`cd.close()`(关闭客户端套接字,确保连接被正确关闭), 直接执行 sys.exit(),导致连接没有正确关闭,从而出现了"can't identify protocol"的问题

#### 为什么lsof和ss的执行结果不同
+ ss命令显示closed状态的socket,在lsof显示的是can't identity protocol
+ 大概是两个命令实现的原理差异吧

#### 通过模拟client断开,server端hang住
+ https://raw.githubusercontent.com/Kingson4Wu/awesome-tools/main/network/http-print.c 加上 `sleep(600);`的代码
+ curl --connect-timeout 10 -m 20 "http://XXXXXXX"
+ 执行lsof 只出现了‘CLOSE_WAIT’,没有出现 can't identity protocol

#### go进程为什么没正确关闭socket
+ todo
+ strace -p

#### strace 怎么使用
+ todo

### gin怎么设置接受的socket数量
+ todo
+ gin接收请求数量上限设置


---

## 处理过程3
+ 分析Go进程死锁原因

+ 通过分析pprof的goroutine堆栈

```go
type GuardPolicy struct {
mu *sync.RWMutex
errData map[*sql.DB]int
}

type sortPool struct {
connPool gorm.ConnPool
errCnt int
}

func (s *GuardPolicy) countErr(db *gorm.DB) {
if db.Error == nil || errors.Is(db.Error, gorm.ErrRecordNotFound) {
return
}
if ins, ok := db.Statement.ConnPool.(*sql.DB); ok {
s.mu.Lock()
defer s.mu.Unlock()
s.errData[ins] = s.errData[ins] + 1
}
}

func (s *GuardPolicy) Resolve(connPools []gorm.ConnPool) gorm.ConnPool {
var x = make([]*sortPool, 0, len(connPools))
for i := range connPools {
p, ok := connPools[i].(*sql.DB)
if !ok {
x = append(x, &sortPool{connPool: connPools[i], errCnt: 0})
} else {
s.mu.RLock()
defer s.mu.RUnlock()
x = append(x, &sortPool{connPool: connPools[i], errCnt: s.errData[p]})
}
}
sort.Slice(x, func(i, j int) bool {
return x[i].errCnt <= x[j].errCnt
})
return x[0].connPool
}

(1) goroutine1: s.mu.RLock()
(2) goroutine2: s.mu.Lock() - 等待(1)
(3) goroutine1: s.mu.RLock() - 等待(2)
相当于goroutine1自己等待自己

  • 问chatgpt:
    是的,提供的代码存在死锁风险。死锁可能发生在Resolve方法中。

在Resolve方法中,对于sql.DB类型的连接,当读取errData映射的错误计数时,使用了RWMutex的读锁(s.mu.RLock())。但是,在循环的每次迭代中都会调用defer s.mu.RUnlock(),这会在for循环结束时解锁,而不是在每次迭代结束时解锁。

这意味着如果在某次循环迭代中获取了读锁,但在获取下一个读锁之前已经进入了循环的下一次迭代,那么这两次迭代将持有读锁,但没有释放,从而导致死锁。

要解决这个问题,可以将读锁的获取和解锁放在同一次循环迭代中,确保每次获取读锁后都会及时释放,而不会在后续迭代中持有读锁。以下是修改后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (s *GuardPolicy) Resolve(connPools []gorm.ConnPool) gorm.ConnPool {
var x = make([]*sortPool, 0, len(connPools))
for i := range connPools {
p, ok := connPools[i].(*sql.DB)
if !ok {
x = append(x, &sortPool{connPool: connPools[i], errCnt: 0})
} else {
s.mu.RLock()
errCnt := s.errData[p]
s.mu.RUnlock()
x = append(x, &sortPool{connPool: connPools[i], errCnt: errCnt})
}
}
sort.Slice(x, func(i, j int) bool {
return x[i].errCnt <= x[j].errCnt
})
return x[0].connPool
}

扩展

  • 跟这个很像:线上一次大量 CLOSE_WAIT 复盘

Reference

  • tcp socket文件句柄泄漏
  • 确认是否有socket泄露
  • 记一次排查socket fd泄漏
  • 又一次排查socket fd泄漏
  • socket句柄泄漏问题的定位: losf和strace的联合使用

我接触过的几种服务发现和rpc

发表于 2023-07-04

公司V

  • 主要Java语言
  • 最原始
  • 没有服务发现
  • 通过域名调用,服务器ip配置在nginx
  • 数据传输使用JSON协议

公司U

  • 主要Java语言
  • 公司基础团队自己实现服务发现机制
  • 服务发现使用netty做tcp长链,用于服务注册等
  • 服务sdk从服务中心获取所调用的对端服务的ip和端口
  • 通过tcp长链通知服务上下线等
  • 通过服务的ip和端口调用
  • 数据传输使用hessian协议

公司K

部门1

版本v1

  • 主要Java语言
  • 使用Eureka做服务发现(HTTP协议)
  • 服务sdk从Eureka获取所调用的对端服务的ip和端口
  • 通过HTTP接口从Eureka定时更新服务实例
  • 通过服务的ip和端口调用
  • 数据传输使用Thrift协议/JSON协议

版本v2

  • 在版本1的基础上增加LocalProxy 作为代理
  • 加入local proxy (service mesh)
    (服务a)->本地LP ->(服务b)

版本v3

  • 在k8s 部署
  • 让每个pod都是单独的实例,ip和物理机一样可以访问,使用calico打通网络,兼容原来的服务发现机制,暂时不使用k8s的service功能
    (服务a)->远程LP ->(服务b)

版本v4

  • 使用Istio替换LP
  • LP(linkerd)使用Euerka,Istio使用k8s service
  • (服务a)->istio-istio →(服务b)
  • 内部调用:(服务a,istio-envoy1)-》(istio-envoy2,服务b)
    ()标识同一个pod

部门2

  • 使用nginx二次开发,作为localproxy,以域名作为服务名
  • 比较原始的方案(公司起步比较早,当时的方案一直沿用至今)

谈谈定时任务的原理和应用

发表于 2023-07-03

定时任务的原理

  • 本质是开一个线程,无限循环,检查本地的当前时间,是否符合执行的条件

  • Redis: 比如redis-5.0.8,起了一个线程不停的循环检查,比对当前时间(gettimeofday)判断是否需要执行

  • Golang: 起一个goroutine无限循环,从最小堆中取符合时间的任务

  • Java: 单线程, 无限循环,最小堆: java.util.TimerThread#mainLoop

定时任务类型

  • 定时任务一般分为 本地定时任务 和 分布式定时任务

  • 比如定时加载数据到本地缓存的时候,这时一般就使用本地定时任务

  • 而有些任务定时只需要执行一次, 现在很多服务都不是单点了,这就需要分布式定时任务来进行调度了

分布式定时任务使用示例

  • 业界上可选择的定时任务工具很多,在工作中使用最多的是Saturn,个人觉得还是很方便的,其他分布式定时任务的原理其实基本大同小异

  • 官网地址: https://github.com/vipshop/Saturn

  • Saturn架构文档

Saturn

  • 如果是Java服务,可以很方便的接入Saturn,可以直接通知到服务
  • 但如果是其他语言,则是通过执行shell命令间接实现的

使用域信号通知通知服务

  • 这里Go应用为例子,通过域信号结合shell命令,从而方便使用saturn定时任务

  • client端:提供shell命令,供saturn定时调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const SockPath = "/tmp/notify-os.sock"

func notifyRun(task *notifyTask) {

httpc := http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", SockPath)
},
},
}
response, err := httpc.Get("http://unix/" + task.name + "?" + task.args)
if err != nil {
panic(err)
}
var buf = make([]byte, 1024)
response.Body.Read(buf)
logger.Infof("finish notify task: %s, args:%s, resp: %s", task.name, task.args, string(buf))
}

  • server端:提供uds服务,介绍请求触发执行相应的任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

type ser struct{}

func (s ser) ServeHTTP(rw http.ResponseWriter, r *http.Request) {

defer func() {
if err := recover(); err != nil {
logger.Errorf("notify server panic, r: %s, err:%s", r.RequestURI, err)
}
}()

name := r.URL.Path

if strings.HasPrefix(name, "/") {
name = name[1:]
}

if job, ok := jobs[name]; ok {
args := map[string]string{}
for k, v := range r.URL.Query() {
args[k] = v[0]
}
if job.Handler(args) {
rw.Write([]byte("ok"))
logger.Infof("job run success, name:%s, args: %s", name, args)
} else {
rw.Write([]byte("fail"))
logger.Errorf("job run fail, name:%s, args: %s", name, args)
}
return
}

rw.Write([]byte("not exist"))
logger.Warnf("job not exist, name:%s", name)

}
func NotifyServe() {
sockPath := utils.BizConfig("notify_uds_file", "")

if sockPath == "" {
panic("sockPath is nil")
}

logger.Info("Unix NotifyServe ...")
os.Remove(sockPath)
server := http.Server{
Handler: ser{},
}
unixListener, err := net.Listen("unix", sockPath)
if err != nil {
panic(err)
}
server.Serve(unixListener)
}

  • windows 平台没有uds,可以使用普通的HTTP接口代替

对工作中接触过的几种编程语言及其部署架构的看法

发表于 2023-07-02

公司V

  • 使用Java作为编程语言

  • 使用Spring-MVC框架

  • 使用Tomcat作为Web容器

  • 打包运行方式:

    1. 服务器预安装了Java和Tomcat
    2. 将代码打包成war包
    3. 将war包上传到服务器的Tomcat的web目录
    4. 重启tomcat更新服务

公司U

  • 使用Java作为编程语言

  • 使用二次开发的Play1.x作为web框架(底层基于Netty)

  • 公司在框架自行研发了服务发现等功能

  • 打包运行方式:

    1. 服务器预安装了Java
    2. 将代码打包成Zip包
    3. 将zip包上传到服务器的指定目录
    4. 通过信号停止服务(kill),通过Java命令启动服务

公司B

  • 使用Java作为编程语言

  • 使用Spring-Boot框架

  • 使用Tomcat作为Web容器

  • 打包运行方式:

    1. 服务器预安装了Java和Tomcat
    2. 将代码打包成war包
    3. 将war包上传到服务器的Tomcat的web目录
    4. 重启tomcat更新服务

公司K

Java

  • 使用Java作为编程语言

  • 使用二次开发的Spring-Boot和Spring-Cloud作为web框架

  • 公司在框架自行研发了服务发现,服务治理,数据库切换、容灾相关等功能

  • 打包运行方式:

    1. 服务器预安装了Java
    2. 将代码打包成Zip包
    3. 将zip包上传到服务器的指定目录
    4. 通过信号停止服务(kill),通过Java命令启动服务

Lua

  • 使用Lua作为编程语言

  • 使用OpenResty作为服务器

  • 公司开发了一些通用的sdk,在OpenResty配置中include即可

  • 打包运行方式:

    1. 服务器预安装了OpenResty
    2. 将代码文件上传到指定的目录
    3. 执行 nginx -s reload 更新OpenResty的lua文件即可

PHP

  • 使用PHP作为编程语言

  • 使用OpenResty作为服务器

  • 打包运行方式:

    1. 服务器预安装了OpenResty和PHP
    2. 将代码文件上传到指定的目录
    3. 执行 nginx -s reload 更新OpenResty的PHP文件即可

Go

  • 使用Go作为编程语言

  • 打包运行方式:

    1. 将代码打包成二进制可执行文件
    2. 使用nohup运行二进制即可(有的使用supervisor,有的使用systemctl)

对这些编程语言和运行方式的理解和看法

Java

  • 编译语言
  • Java服务需要运行在JVM上,而JVM是使用C++实现的
  • Java代码编译后本质上是一堆字节码文件,不具备单独运行的能力
  • 运行Java字节码,需要JRE环境

PHP

  • 脚本语言
  • PHP通常和Web服务器搭配(比如Apache和Nginx),对外提供Web服务
  • 以Nginx为例,Nginx不能直接处理PHP脚本,需要使用FastCGI协议将请求转发到PHP解释器
    (因此,首先需要安装PHP和FastCGI进程管理器,例如php-fpm)
  • 通常情况下,PHP解释器会安装在机器本机上。当Nginx通过FastCGI协议与PHP解释器进行通信时,它会将请求转发到运行在本机的FastCGI进程管理器,然后再将请求转发到本机上运行的PHP解释器

Lua

  • 脚本语言
  • 以OpenResty作为web服务器运行Lua脚本是一种方式
  • Nginx + LuaJIT = OpenResty

OpenResty

  • OpenResty本质上是将 LuaJIT 的虚拟机嵌入到 Nginx 的管理进程和工作进程中,同一个进程内的所有协程都会共享这个虚拟机,并在虚拟机中执行Lua代码。在性能上,OpenResty接近或超过 Nginx 的C模块,而且开发效率更高。
  • OpenResty中的核心技术cosocket将 Lua 协程和 Nginx 的事件机制结合在一起,最终实现了非阻塞网络IO。不仅和HTTP客户端之间的网络通信是非阻塞的,与MySQL、Memcached以及Redis等众多后端之间的网络通信也是非阻塞的。

Openresty 是怎么嵌入lua虚拟机的

  • OpenResty 是一个基于 Nginx 的 Web 应用服务器,它可以通过嵌入 Lua 脚本语言来实现高性能的 Web 应用程序。OpenResty 在运行过程中通过 LuaJIT 虚拟机来解释执行 Lua 代码。
  • OpenResty 在编译时会将 LuaJIT 虚拟机源代码打包成动态链接库,然后将其嵌入到 Nginx 中。OpenResty 运行时会通过 LuaJIT 虚拟机来加载和执行 Lua 代码,从而实现了高性能的 Web 应用程序。

Go

  • 编译语言
  • 编译成二进制文件后,可以直接在服务器运行,相比其他语言,一般可以认为没其他依赖

个人看法

  • Lua比较小众,不适合做大型的业务开发,而且debug比较不方便;
  • PHP在业界已经是快被淘汰的语言了,不做仔细研究,初步体验感觉一般(可能是受限于公司的维护环境过于糟糕,各种老版本,和高深莫测的代码);
  • Java是我职业生涯接触最久的编程语言,谈不上喜欢也谈不上讨厌,但自从学习了Go之后,有了新的体会;
  • 当初学习Go的其中一个原因是自己的个人项目比较轻量,Go其中一个优势是不依赖其他软件就可以运行,而且分发方便;
  • Go使用AOT的方式编译,针对特定平台,除了加快启动速度,也使程序打包体积大大减少;
  • 当然Java也可以用graalvm打成二进制文件启动(没尝试过,感觉还是有不少前置条件);
  • 除此之外,还有很多其他以后补充
<1…567…17>

166 日志
191 标签
RSS
© 2025 Kingson Wu
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4