02|小菜成长之路,警惕沦为 API 调用侠
小菜(化名)在某互联网公司担任运维工程师,负责公司后台业务的运维保障工作。由于自己编程经验不多,平时有不少工作需要开发协助。
听说 Python 很火,能快速开发一些运维脚本,小菜也加入 Python 大军学起来。Python 语言确实简单,小菜很快就上手了,觉得自己应对运维开发工作已经绰绰有余,便不再深入研究。
背景
这天老板给小菜派了一个数据采集任务,要实时统计服务器 TCP 连接数。
需求背景是这样的:开发同事需要知道服务的连接数以及不同状态连接的比例,以便判断服务状态。因此,小菜需要开发一个脚本,定期采集并报告 TCP 连接数,提交数据格式定为 json:
{"LISTEN": 4,"ESTABLISHED": 100,"TIME_WAIT": 10
}
作为运维工程师,小菜当然知道怎么查看系统 TCP 连接。Linux 系统中有两个命令可以办到,netstat 和ss:
$ netstat -nat
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 127.0.0.1:8388 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp 0 0 192.168.56.3:22 192.168.56.1:54983 ESTABLISHED
tcp6 0 0 :::22 :::* LISTEN
$ ss -nat
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 127.0.0.1:8388 0.0.0.0:*
LISTEN 0 128 127.0.0.53%lo:53 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
ESTAB 0 0 192.168.56.3:22 192.168.56.1:54983
LISTEN 0 128 [::]:22 [::]:*
小菜还知道 ss 命令比 netstat 命令要快,但至于为什么,小菜就不知道了。小菜很快找到老板,提出了自己的解决方案:写一个 Python 程序,调用 ss 命令采集 TCP 连接信息,然后再逐条统计。
老板告诉小菜,线上服务器很多都是最小化安装,并不能保证每台机器上都有ss或者netstat命令。老板还告诉小菜,程序开发要学会站在巨人的肩膀上。动手写代码前,先调研一番,看是否有现成的解决方案。切忌重复造轮子,浪费时间不说,可能代码质量还差,效果也不好。
最后老板给小菜指了条明路,让他回去再看看 psutil。psutil 是一个 Python 第三方包,用于采集系统性能数据,包括:CPU、内存、磁盘、网卡以及进程等等。临走前,老板还叮嘱小菜,完成工作后花点时间研究下这个库。
psutil 方案
小菜搜索 psutil 发现,还有这么顺手的第三方库,喜出望外!他立马装好 psutil,准备开干:
$ pip install psutil
导入 psutil 后,一个函数调用就可以拿到系统所有连接,连接信息非常丰富:
>>> import psutil
>>> for conn in psutil.net_connections('tcp'):
... print(conn)
...
sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='192.168.56.3', port=22), raddr=addr(ip='192.168.56.1', port=54983), status='ESTABLISHED', pid=None)
sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='127.0.0.1', port=8388), raddr=(), status='LISTEN', pid=None)
sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='0.0.0.0', port=22), raddr=(), status='LISTEN', pid=None)
sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='127.0.0.53', port=53), raddr=(), status='LISTEN', pid=None)
sconn(fd=-1, family=<AddressFamily.AF_INET6: 10>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='::', port=22), raddr=(), status='LISTEN', pid=None)
小菜很满意,感觉不用花多少时间就可搞定数据采集需求了,准时下班有望!噼里啪啦,很快小菜就写下这段代码:
import psutil
from collections import defaultdict# 遍历每个连接,按连接状态累加
stats = defaultdict(int)
for conn in psutil.net_connections('tcp'):stats[conn.status] += 1# 遍历每种状态,输出连接数
for status, count in stats.items():print(status, count)
小菜接着在服务器上测试这段代码,功能完全正常:
ESTABLISHED 1
LISTEN 4
小菜将数据采集脚本提交,并按既定节奏逐步发布到生产服务器上。开发同事很快就看到小菜采集的数据,都夸小菜能力不错,需求完成得很及时。小菜也很高兴,感觉 Python 没白学。如果用其他语言开发,说不定现在还在加班加点呢!Life is short, use Python! 果然没错!
小菜愈发自信,早就把老板的话抛到脑后了。psutil 这个库这么好上手,有啥好深入研究的?
内存悲剧
突然有一天,其他同事紧急告诉小菜,他开发的采集脚本占用很多内存,CPU 也跑到了 100%,已经开始影响线上服务了。小菜还沉浸在成功的喜悦中,收到这个反馈如同晴天霹雳,有点举手无措。
业务同事告诉小菜,受影响的机器系统连接数非常大,质疑小菜是不是脚本存在性能问题。小菜觉得很背,脚本只是调用 psutil 并统计数据,怎么可能存在性能问题? 脚本影响线上服务,小菜压力很大,但不知道如何是好,只能跑去找老板寻求帮助。
老板要小菜第一时间停止数据采集,降低影响。复盘故障时,老板很敏锐地问小菜,是不是用容器保存所有连接了?小菜自己并没有,但是 psutil 这么做了:
>>> psutil.net_connections()
[sconn(fd=-1, family=<AddressFamily.AF_INET6: 10>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='::', port=22), raddr=(), status='LISTEN', pid=None), sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='0.0.0.0', port=22), raddr=(), status='LISTEN', pid=None), sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='127.0.0.53', port=53), raddr=(), status='LISTEN', pid=None), sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_DGRAM: 2>, laddr=addr(ip='10.0.2.15', port=68), raddr=(), status='NONE', pid=None), sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_DGRAM: 2>, laddr=addr(ip='127.0.0.1', port=8388), raddr=(), status='NONE', pid=None), sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='192.168.56.3', port=22), raddr=addr(ip='192.168.56.1', port=54983), status='ESTABLISHED', pid=None), sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_DGRAM: 2>, laddr=addr(ip='127.0.0.53', port=53), raddr=(), status='NONE', pid=None), sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='127.0.0.1', port=8388), raddr=(), status='LISTEN', pid=None)]
psutil 将采集到的所有 TCP 连接放在一个列表里返回。如果服务器上有十万个 TCP 连接,那么列表里将有十万个连接对象。难怪采集脚本吃了那么多内存!
老板告诉小菜,可以用生成器加以解决。与列表不同,生成器逐个返回数据,因此不会占用太多内存。 Python2 中 range 和 xrange 函数的区别也是一样的道理。
小菜从 pstuil fork 了一个分支,并将 net_connections 函数改造成 生成器 :
def net_connections():while True:if done:break# 解析一个TCP连接conn = xxxyield conn
代码上线后,采集脚本内存占用量果然下降了!生成器将统计算法的空间复杂度由原来的 O(n) 优化为O(1)。经过这次教训,小菜不敢再盲目自信了,他决定抽时间好好看看 psutil 的源码。
源码体会
深入学习源码后,小菜发现原来 psutil 采集 TCP 连接数的秘笈是:从 /proc/net/tcp
以及 /proc/net/tcp6
读取连接信息。由此,他还进一步了解到 procfs,这是一个伪文件系统,将内核空间信息以文件方式暴露到用户空间。/proc/net/tcp
文件则是提供内核 TCP 连接信息:
$ cat /proc/net/tcpsl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode0: 0100007F:20C4 00000000:0000 0A 00000000:00000000 00:00000000 00000000 65534 0 18183 1 0000000000000000 100 0 0 10 01: 3500007F:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000 101 0 16624 1 0000000000000000 100 0 0 10 02: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 18967 1 0000000000000000 100 0 0 10 03: 0338A8C0:0016 0138A8C0:D6C7 01 00000000:00000000 02:00023B11 00000000 0 0 22284 4 0000000000000000 20 13 23 10 20
小菜还注意到,连接信息看起来像个自定义类对象,但其实是一个 namedtuple:
# psutil.net_connections()
sconn = namedtuple('sconn', ['fd', 'family', 'type', 'laddr', 'raddr','status', 'pid'])
小菜一开始并不知道作者为啥要这么做。后来,小菜开始研究 Python 源码,学习了 Python 类机制后他恍然大悟。Python 自定义类的每个实例对象均需要一个 dict 来保存对象属性,这也就是对象的属性空间。如果用自定义类来实现,每个连接都需要创建一个字典,而字典又是散列表实现的。如果系统存在成千上万的连接,开销可想而知。
小菜将学到的知识总结起来:对于数量大而属性固定的实体,没有必要用自定义类来实现,用 namedtuple更合适,开销更小。由此,小菜不经由衷佩服 psutil 的作者。
CPU悲剧
后来小菜又收到业务反馈,采集脚本在高并发的服务器上,CPU 使用率很高,需要再优化一下。小菜回忆psutil 源码,很快就找到了性能瓶颈处:psutil 将连接信息所有字段都解析了,而采集脚本只需要其中的状态字段而已。跟老板商量后,小菜决定自行读取 procfs 来实现采集脚本,只解析状态字段,避免不必要的计算开销。「只提取有需要的内容,不需要的内容舍去」
procfs 方案
直接读取 /proc/net/tcp
,可以得到完整的 TCP 连接信息:
>>> with open('/proc/net/tcp') as f:
... for line in f:
... print(line.rstrip())
...sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode0: 0100007F:20C4 00000000:0000 0A 00000000:00000000 00:00000000 00000000 65534 0 18183 1 0000000000000000 100 0 0 10 01: 3500007F:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000 101 0 16624 1 0000000000000000 100 0 0 10 02: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 18967 1 0000000000000000 100 0 0 10 03: 0338A8C0:0016 0138A8C0:D6C7 01 00000000:00000000 02:0007169E 00000000 0 0 22284 3 0000000000000000 20 20 33 10 20
其中,IP、端口、状态等字段都是以十六进制编码的。例如,st 列表示状态,状态码 0A 表示 LISTEN。很快小菜就写下这段代码:
from collections import defaultdictstat_names = {'0A': 'LISTEN','01': 'ESTABLISHED',# ...
}# 遍历每个连接,按连接状态累加
stats = defaultdict(int)with open('/proc/net/tcp') as f:# 跳过表头行f.readline()for line in f:st = line.strip().split()[3]stats[st] += 1for st, count in stats.items():print(stat_names[st], count)
现在,小菜写代码比之前讲究多了。在统计连接数时,他并不急于将状态码解析成名字,而是按原样统计。等统计完成,他再一次性转换,这样状态码转换开销便降到最低:O(1) 而不是 O(n)。
这次改进符合业务同事预期,但小菜决定好好做一遍性能测试,不打无准备之仗。他找业务同事要了一个连接数最大的 /proc/net/tcp
样本,拉到本地测试。测试结果还算符合预期,采集脚本能够扛住十万连接采集压力。
性能测试中,小菜发现了一个比较奇怪的问题。同样的连接规模,把 /proc/net/tcp
拉到本地跑比直接在服务器上跑要快,而本地电脑性能肯定比不上服务器。他百思不得其解,又去找老板帮忙。
老板很快指出到其中的区别,将 /proc/net/tcp
拉到本地就成为普通磁盘文件,而 procfs 是内核映射出来的伪文件,并不是磁盘文件。他让小菜研究一下 Python 文件 IO 以及内核 IO 子系统在处理这两种文件时有什么区别,还让小菜特别留意 IO 缓冲区大小。
IO 缓冲
小菜打开一个普通的磁盘文件,发现 Python 选的默认缓冲区大小是 4K(读缓存对象头152字节):
>>> f = open('test.py')
>>> f.buffer.__sizeof__()
4248
但是如果打开的是 procfs 文件,Python 选的缓冲区却只有 1K,相差了4倍呢!
>>> f = open('/proc/net/tcp')
>>> f.buffer.__sizeof__()
1176
因此,理论上 Python 默认读取 procfs 发生的上下文切换次数是普通磁盘文件的 4倍,怪不得会慢。虽然小菜还不知道这种现象背后的原因,但是他已经知道怎么进行优化了。随即他决定将缓冲区设置为 1M以上,尽量避免 IO 上下文切换,以空间换时间:
with open('/proc/net/tcp', buffering=1*1024*1024) as f:# ...
经过这次优化,采集脚本在大部分服务器上运行良好,基本可以高枕无忧了。而小菜也意识到编程语言以及操作系统等底层基础知识的重要性,他开始制定学习计划补全计算机基础知识。
netlink 方案
后来负载均衡团队找到小菜,他们也想统计服务器上的连接信息。由于负载均衡服务器作为入口转发流量,连接数规模特别大,达到几十万,将近百万的规模。小菜决定好好进行性能测试,再视情况上线。
测试结果并不乐观,采集脚本要跑几十秒钟才完成,CPU 跑到 100%。小菜再次调高 IO 缓冲区,但效果不明显。小菜又测试了 ss 命令,发现 ss 命令要快很多。由于之前尝到了阅读源码的甜头,小菜很想到 ss 源码中寻找秘密。
由于项目时间较紧,老板提醒小菜先用 strace 命令追踪 ss 命令的系统调用,便可快速获悉 ss 的实现方式。老板演示了 strace 命令的用法,很快就找到了 ss 的秘密——Netlink:
$ strace ss -nat
...
socket(AF_NETLINK, SOCK_RAW|SOCK_CLOEXEC, NETLINK_SOCK_DIAG) = 3
...
Netlink套接字是 Linux 提供通讯机制,可用于内核与进程间、进程与进程间通讯。Netlink 下的 sock_diag 子系统,提供了一种从内核获取套接字信息的新方式。
与 procfs 不同,sock_diag 采用网络通讯的方式,内核作为服务端接收客户端进程查询请求,并以二进制数据包响应查询结果,效率更高。这就是 ss 比 netstat 更快的原因,ss 采用 Netlink 机制,而 netstat 采用 procfs 机制。
很不幸 Python 并没有提供 Netlink API,一般人可能又要干着急了。好在小菜先前有意识地研究了部分Python 源码,对 Python 的运行机制有所了解。他知道可以用 C 写一个 Python 扩展模块,在 C 语言中调用原生系统调用。
编写 Python C 扩展模块可不简单,对编程功底要求很高,必须全面掌握 Python 运行机制,特别是对象内存管理。一朝不慎可能导致程序异常退出、内存泄露等棘手问题。好在小菜已经不是当年的小菜了,他经受住了考验。
小菜的扩展模块上线后,效果非常好,顶住了百万级连接的采集压力。一个看似简单得不能再简单的数据采集需求,背后涉及的知识可真不少,没有一定的水平还真搞不定。好在小菜成长很快,他最终还是彻底地解决了性能问题,找回了久违的信心。
内核模块方案
虽然性能问题已经彻底解决,小菜还是没有将其淡忘。他时常想:如果可以将统计逻辑放在内核空间做,就不用在内核和进程之间传递大量连接信息了,效率应该是最高的!受限于当时的知识水平,小菜还没有能力实现这个设想。
后来小菜在研究 Linux 内核时,发现可以用内核模块来扩展内核的功能,结合 procfs 的工作原理,他找到了技术方案!他顺着 /proc/net/tcp
在内核中的实现源码,依样画葫芦写了这个内核模块:
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <net/tcp.h>MODULE_LICENSE("GPL");
MODULE_AUTHOR("Xiaocai");
MODULE_DESCRIPTION("TCP state statistics");
MODULE_VERSION("1.0");// 状态名列表
static char *state_names[] = {NULL,"ESTABLISHED","SYN_SENT","SYN_RECV","FIN_WAIT1","FIN_WAIT2","TIME_WAIT","CLOSE","CLOSE_WAIT","LAST_ACK","LISTEN","CLOSING",NULL
};static void stat_sock_list(struct hlist_nulls_head *head, spinlock_t *lock,unsigned int state_counters[])
{// 套接字节点指针(用于遍历)struct sock *sk;struct hlist_nulls_node *node;// 链表为空直接返回if (hlist_nulls_empty(head)) {return;}// 自旋锁锁定spin_lock_bh(lock);// 遍历套接字链表sk = sk_nulls_head(head);sk_nulls_for_each_from(sk, node) {if (sk->sk_state < TCP_MAX_STATES) {// 自增状态计数器state_counters[sk->sk_state]++;}}// 自旋锁解锁spin_unlock_bh(lock);
}static int tcpstat_seq_show(struct seq_file *seq, void *v)
{// 状态计数器unsigned int state_counters[TCP_MAX_STATES] = { 0 };unsigned int state;// TCP套接字哈希槽序号unsigned int bucket;// 先遍历Listen状态for (bucket = 0; bucket < INET_LHTABLE_SIZE; bucket++) {struct inet_listen_hashbucket *ilb;// 哈希槽ilb = &tcp_hashinfo.listening_hash[bucket];// 遍历链表并统计stat_sock_list(&ilb->head, &ilb->lock, state_counters);}// 遍历其他状态for (bucket = 0; bucket < tcp_hashinfo.ehash_mask; bucket++) {struct inet_ehash_bucket *ilb;spinlock_t *lock;// 哈希槽链表ilb = &tcp_hashinfo.ehash[bucket];// 保护锁lock = inet_ehash_lockp(&tcp_hashinfo, bucket);// 遍历链表并统计stat_sock_list(&ilb->chain, lock, state_counters);}// 遍历状态输出统计值for (state = TCP_ESTABLISHED; state < TCP_MAX_STATES; state++) {seq_printf(seq, "%-12s: %d\n", state_names[state], state_counters[state]);}return 0;
}static int tcpstat_seq_open(struct inode *inode, struct file *file)
{return single_open(file, tcpstat_seq_show, NULL);
}static const struct file_operations tcpstat_file_ops = {.owner = THIS_MODULE,.open = tcpstat_seq_open,.read = seq_read,.llseek = seq_lseek,.release = single_release
};static __init int tcpstat_init(void)
{proc_create("tcpstat", 0, NULL, &tcpstat_file_ops);return 0;
}static __exit void tcpstat_exit(void)
{remove_proc_entry("tcpstat", NULL);
}module_init(tcpstat_init);
module_exit(tcpstat_exit);
内核模块编译好并加载到内核后,procfs 文件系统提供了一个新文件 /proc/tcpstat
,内容为统计结果:
$ cat /proc/tcpstat
ESTABLISHED : 5
SYN_SENT : 0
SYN_RECV : 0
FIN_WAIT1 : 0
FIN_WAIT2 : 0
TIME_WAIT : 1
CLOSE : 0
CLOSE_WAIT : 0
LAST_ACK : 0
LISTEN : 14
CLOSING : 0
当用户程序读取这个文件时,内核虚拟文件系统 (VFS) 调用小菜在内核模块中写的处理函数:遍历内核 TCP 套接字完成统计并格式化统计结果。内核模块、VFS 以及套接字等知识超出专栏范围,不再赘述。
小菜在服务器上试验这个内核模块,真的快得飞起!
经验总结
小菜开始总结这次脚本开发工作中的经验教训,他列出了以下关键节点:
- 依靠 psutil 采集,没有关注 psutil 实现导致性能问题;
- 用生成器代替列表返回连接信息,解决内存瓶颈;
- 直接读取 procfs 文件系统,部分解决 CPU 性能瓶颈;
- 通过调节 IO 缓冲区大小,进一步降低 CPU 开销;
- 用 Netlink 代替 procfs,彻底解决性能问题;
- 实验内核模块思路,终极解决方案快得飞起;
这些问题节点,一个比一个深入,没有一定功底是搞不定的。小菜从刚开始跌跌撞撞,到后来独当一面,快速成长的关键在于善于在问题中总结经验教训:
- 程序开发完一定要做性能测试,看能够扛住多大的压力;
- 使用任何工具,需要准确理解其背后的原理,避免误用;
- 对编程语言以及操作系统源码要保持好奇心;
- 计算机基础知识很重要,需要及时补全才能达到新高度;
- 学会问题发散,举一反三;
02|小菜成长之路,警惕沦为 API 调用侠相关推荐
- python socket tcp6_小菜成长之路,警惕沦为 API 调用侠
小菜(化名)在某互联网公司担任运维工程师,负责公司后台业务的运维保障工作.由于自己编程经验不多,平时有不少工作需要开发协助. 听说 Python 很火,能快速开发一些运维脚本,小菜也加入 Python ...
- redis成长之路——(一)
为什么使用redis Redis适合所有数据in-momory的场景,虽然Redis也提供持久化功能,但实际更多的是一个disk-backed的功能,跟传统意义上的持久化有比较大的差别,那么可能大家就 ...
- 计算机达人成长之路 目录
计算机达人成长之路 木鸿飞就是芸芸众生中推动历史年轮中的微小一员而已,他不是叱诧风云的人物,没有引领时代的潮流,但却走出了自己的计算机之路. "我是为计算机而生的."木鸿飞在日记中 ...
- 程序员成长之路(四)之有用的网址
2019独角兽企业重金招聘Python工程师标准>>> 通过Java来测试JSON和Protocol Buffer的传输文件大小 http://www.jb51.net/articl ...
- 计算机达人成长之路目录
计算机达人成长之路 木鸿飞就是芸芸众生中推动历史年轮中的微小一员而已,他不是叱诧风云的人物,没有引领时代的潮流,但却走出了自己的计算机之路. "我是为计算机而生的."木鸿飞在日记中 ...
- 架构师成长之路:如何提升技术掌控力?
架构师成长之路:如何提升技术掌控力? 简介: 在很多人眼里,架构师就犹如古代的将军一般,既能运筹帷幄决胜千里,又能独闯敌营取人首级,是所有士兵们崇拜的偶像...好了,其实我只是想说:能成为一名优秀的架 ...
- python linux运维教程 推荐_Linux运维人员成长之路学习书籍推荐
原标题:Linux运维人员成长之路学习书籍推荐 一.入门书: <鸟哥的私房菜(基础篇)> <鸟哥的私房菜(服务篇)> <Linux命令行与Shell脚本编程大全(第2版) ...
- 一个大神的Android成长之路
这篇文章是我的一个朋友写的,总结了这些年的技术成长之路,我觉得对于很多技术人都有借鉴的作用,技术是相通的,不要整天想一口气吃成一个胖子,不积跬步无以至千里,既然选择了技术这条路,就不畏艰辛,苦中有甜, ...
- 我的前端成长之路:中医药大学毕业的业务女前端修炼之路
简介: 前端工程师的修炼没有捷径,踏踏实实的通过一个个项目的实践来升级打怪实现进阶:本文仅分享自己11年的前端生涯,探讨一直在业务中的技术人的成长之路,也复盘再认识下自己,每个节点我遇到的问题和我的选 ...
最新文章
- Elasticsearch的前后台运行与停止(rpm包方式)
- Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC
- 光影的魔法!Cocos Creator 实现屏幕空间的环境光遮蔽(SSAO)
- ubuntu问题解答集锦
- 要判断一个飞鸽传书2007是不是好的
- 数据分析常用的python包_量化投资数据分析之常用的python包(附代码)
- hdu 1671 Phone List (字典树)
- MySQL-第十篇多表连接查询
- win7系统修复工具_Windows Repair Pro v4.4.60 系统修复工具
- 怎么把照片背景换掉?如何给照片换底色?
- 湖南人,霸占互联网的三分天下
- SpringBoot系列之(一):入门
- Selenium基础用法
- WESTCAR系列的液力偶合器rotofluid、rotomec、kda
- 获取拼音首字母(含生僻字)工具类
- 计算机学后感作文400,科技展观后感作文400字(精选7篇)
- Python---GPA(绩点)计算器
- stm32f429基于ymodem传输的bootloader
- CSDN如何置顶博客
- java下载微信支付账单_java微信支付,对账单下载
热门文章
- 我们的GAME-TECH沙龙北京站完美收官了,都讨论了些啥?
- AOE工程实践-银行卡OCR里的图像处理
- GPUImage滤镜实战
- yjk只算弹性的不计算弹塑性_YJK-ABAQUS接口软件使用说明
- v74.01 鸿蒙内核源码分析(编码方式篇) | 机器指令是如何编码的 | 百篇博客分析OpenHarmony源码
- 投入3.6亿美元!加拿大启动国家量子战略
- CentOS 7.X升级至CentOS 8.2
- C语言宏定义制作函数模板
- Transfiguring Portraits论文阅读笔记
- touch事件 以及 点击穿透的三种解决方法(移动端)