问题模型


tcp三次握手建立过程

问题1:当n多个client在极短时间内同时发出多个syn=1请求建立连接时

这时server端内核中的syn队列会发生溢出,部分连接请求会丢失。使用dmesg打印内核环形缓冲区会得到如下输出:possible syn flooding on port 30005

出现这样的原因是因为syn队列设置的太小,使得短时间内syn队列溢出,让部分连接请求丢失。

使用nstat可查看SNMP计数器。
TcpExtListenOverflows:Accept queue队列超过上限时加1
TcpExtListenDrops:任何原因,包括Accept queue超限,创建新连接,继承端口失败等,加1

nstat -az TcpExtListenOverflows TcpExtListenDrops

使用如下命令可以查看当前内核中syn队列大小。

cat /proc/sys/net/ipv4/tcp_max_syn_backlog

解决方案1:扩大服务端SYN队列大小,减少连接请求失效数

修改/etc/sysctl.conf文件,在其末尾添加上net.ipv4.tcp_max_syn_backlog = 4096

sudo vim /etc/sysctl.conf
sudo /sbin/sysctl -p //使sysctl.conf配置立即生效。

解决方案2:启用服务端tcp_syncookies,对那些无法放入syn队列请求,使用syn-cookies的方式处理

修改/etc/sysctl.conf文件,在其末尾添加net.ipv4.tcp_syncookies = 1

sudo vim /etc/sysctl.conf
sudo /sbin/sysctl -p //让配置生效

解决方案3:减少客户端SYN的retry次数,提前返回错误至客户端,让客户端处理,从源头上解决。

在客户端中的/etc/sysctl.conf文件末尾添加net.ipv4.tcp_syn_retries = 2

sudo vim /etc/sysctl.conf
sudo /sbin/sysctl -p //让配置生效

解决方案4:减少服务端SYN队列中syn+ack尝试次数,让服务器尽早释放SYN队列多次尝试的连接,腾出SYN队列空位。

在服务端中的/etc/sysctl.conf文件末尾添加net.ipv4.tcp_synack_retries = 2

sudo vim /etc/sysctl.conf
sudo /sbin/sysctl -p //让配置生效

问题2:当syn队列中的请求完成SYN_RCVD并收到来自客户端的ACK后,将SYN队列中的socket添加到acp队列,但应用层调用accept速度不够快,使得acp队列溢出。

同问题1,可以通过SNMP计数器来观察是否存在acp队列存在溢出情况。

解决方案1:扩大acp队列大小,减少连接请求失效数

使用如下命令可查看因为acp队列满,而丢失的SYN队列请求

netstat -s |grep "SYNs to LISTEN"

在linux2.2之前,syn+acp队列的总大小由listen(fd,backlog)函数中的backlog确定,但在linux2.2之后,listen中的backlog仅仅指定acp队列大小。
使用如下命令,可以看到如下提示。

man listen


因此可以通过修改backlog参数来扩大acp队列大小,但这还不够,紧接着下面这句话。

因此可以得知acp队列实际大小为min(backlog,somaxconn),这是为什么仅仅修改backlog参数,并不会使得acp队列扩大的原因。

同样的修改/etc/sysctl.conf,在其后面添加net.core.somaxconn = 4096

sudo vim /etc/sysctl.conf
sudo /sbin/sysctl -p //使配置立即生效

解决方案2:启用tcp_abort_on_overflow,对那些在syn队列中完成了三次握手,即将从syn队列加入到acp队列的请求直接丢弃。

修改/etc/sysctl.conf文件,在其末尾添加net.ipv4.tcp_abort_on_overflow = 1

sudo vim /etc/sysctl.conf
sudo /sbin/sysctl -p //使配置立即生效

若启用该选项,那么会部分客户端会出现104 Connection reset by peer的错误。

内核中的问题1和问题2

在sock结构体中,存有两个 unsigned short变量,一个sk_ack_backlog,一个sk_max_ack_backlog,这两个变量标识了问题模型中的acp队列的大小。

struct sock{  ....unsigned short      sk_ack_backlog;  unsigned short      sk_max_ack_backlog;  .....
}

当client发送SYN=1的报文到服务端后,服务端进行如下调用
->tcp_v4_rcv
->tcp_v4_do_rcv
->tcp_rcv_state_process
->tcp_v4_conn_request
->tcp_conn_request

最后在tcp_conn_request中调用inet_reqsk_alloc创建一个request_sock(不占用文件描述符),并将其加入到SYN队列中。

int tcp_conn_request(...)
{...//分配request_sockreq = inet_reqsk_alloc(rsk_ops, sk, !want_cookie);
...//将req加入到syn队列中 inet_csk_reqsk_queue_hash_add(sk, req,tcp_timeout_init((struct sock *)req));  //发送syn+ack tcp_v4_send_synackaf_ops->send_synack(sk, dst, &fl, req, &foc,!want_cookie ? TCP_SYNACK_NORMAL :TCP_SYNACK_COOKIE);
...
}

修订该部分内容已更新,syn队列实际实现早已不是这样的
查看最新blog:https://editor.csdn.net/md/?articleId=114981573
在listen_sock结构体中可以看到存储request_sock的实体*request_sock syn_table[0]; 。其实际路径为
->inet_csk_reqsk_queue_hash_add()//函数
->icsk_accept_queue//变量名
->struct request_sock_queue icsk_accept_queue;//变量名的定义
->struct request_sock_queue//结构体
->struct listen_sock *listen_opt;
->listen_opt
->struct request_sock *syn_table[0];

struct inet_connection_sock {/* inet_sock has to be the first member! */struct inet_sock          icsk_inet;struct request_sock_queue icsk_accept_queue;
}
struct request_sock_queue {
/*Points to the request_sock accept queue, when after 3 handshake will add the request_sock from syn_table to here*/  struct request_sock    *rskq_accept_head;  struct request_sock    *rskq_accept_tail;  rwlock_t        syn_wait_lock;  u8            rskq_defer_accept;  /* 3 bytes hole, try to pack */  struct listen_sock    *listen_opt;
};
struct listen_sock {  u8            max_qlen_log; /*2^max_qlen_log is the length of the accpet queue, max of max_qlen_log is 10. (2^10=1024)*/  /* 3 bytes hole, try to use */  int            qlen; /* qlen is the current length of the accpet queue*/  int            qlen_young;  int            clock_hand;  u32            hash_rnd;  u32            nr_table_entries; /*nr_table_entries is the number of the syn_table,max is 512*/  struct request_sock    *syn_table[0];
};

最终能够看到syn队列长度的定义max_qlen_log。

在tcp_v4_conn_request函数中,能够看到内核时如何丢弃报文的。其中使用inet_csk_reqsk_queue_is_full判断syn队列是否满,如果满且没有配置tcp_syncookies 那么就直接丢弃goto drop。
sysctl_tcp_syncookies = 2无条件生成syn-cookie,现在内核都默认开启了sysctl_tcp_syncookies =1,一旦当syn队列溢出后,就会立即启用syncookies来处理syn队列溢出的情况。

int tcp_conn_request(struct sock *sk, struct sk_buff *skb)
{...if ((net->ipv4.sysctl_tcp_syncookies == 2 ||inet_csk_reqsk_queue_is_full(sk)) && !isn) {want_cookie = tcp_syn_flood_action(sk, skb, rsk_ops->slab_name);if (!want_cookie)goto drop;}...if (want_cookie) {#ifdef CONFIG_SYN_COOKIESsyn_flood_warning(skb);  // 输出 "possible SYN flooding on port %d. Sending cookies.\n"req->cookie_ts = tmp_opt.tstamp_ok;  // 为当前 socket 设置启用 cookie 标识#endif// 生成 syncookieisn = cookie_v4_init_sequence(sk, skb, &req->mss);} else if (!isn) {...}
...
}

在 tcp 三次握手完成后,将连接置入 ESTABLISHED 状态并交付给应用程序的 acp队列时,会检查 acp队列是否已满。若已满,通常行为是将连接还原至 SYN_ACK 状态,以造成 3 路握手最后的 ACK 包意外丢失假象, 这样在客户端等待超时后可重发 ACK , 以再次尝试进入 ESTABLISHED 状态 ,作为一种修复/重试机制。

struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,struct request_sock *req,struct request_sock **prev)
{.../* OK, ACK is valid, create big socket and* feed this segment to it. It will repeat all* the tests. THIS SEGMENT MUST MOVE SOCKET TO* ESTABLISHED STATE. If it will be dropped after* socket is created, wait for troubles.*/// 调用 net/ipv4/tcp_ipv4.c 中的 tcp_v4_syn_recv_sock 函数// 判定 accept queue 是否已经满,若已满,则返回的 child 为 NULLchild = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL);if (child == NULL)goto listen_overflow;// 在 accept queue 未满的情况下,将 ESTABLISHED 连接从 SYN queue 搬移到 accept queue 中inet_csk_reqsk_queue_unlink(sk, req, prev);inet_csk_reqsk_queue_removed(sk, req);inet_csk_reqsk_queue_add(sk, req, child);return child;listen_overflow:// 若 accept queue 已满,但设置的是 net.ipv4.tcp_abort_on_overflow = 0if (!sysctl_tcp_abort_on_overflow) {inet_rsk(req)->acked = 1;    // 则只标记为 acked ,return NULL;}embryonic_reset:// 若 accept queue 已满,但设置的是 net.ipv4.tcp_abort_on_overflow = 1NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_EMBRYONICRSTS);   if (!(flg & TCP_FLAG_RST))req->rsk_ops->send_reset(sk, skb);   inet_csk_reqsk_queue_drop(sk, req, prev);// 从 SYN queue 中删除该连接信息return NULL;
}

问题3.从acp队列中取出socket fd并生成文件fd绑定到进程时,因为acp队列中的socket fd太多,使得文件fd用完。

在服务端使用accept函数从acp队列出取出已经确认连接后的socket,并适用于sock_map_fd(),将socket中的file映射到文件fd上(应该是映射到网卡上,这样就可以使用read,write等操做)

struct socket
{socket_state  state; // socket stateshort   type ; // socket typeunsigned long  flags; // socket flagsstruct fasync_struct  *fasync_list;wait_queue_head_t wait;struct file *file;struct sock *sock; const struct proto_ops *ops;
}

->sock_map_fd
->get_unused_fd_flag
-> __alloc_fd(current->files, 0, rlimit(RLIMIT_NOFILE), flags)
->newfile = sock_alloc_file(sock, flags, NULL);
->fd_install(fd,newfile )
其中RLIMIT_NOFILE为进程最大可打开的文件描述符个数,如果大于该数,那么将会直接返回错误,连接不能创立成功。

使用ulimit -a命令能够查看当前能够最大打开文件描述的个数

ulimit -a


通常这个数目默认为1024,对于需要建立大量连接的服务器来说,这显然是不够的。当连接数目过多,连接将无法成功建立,并会抛出错误too many open files。

如果fd能够正常获取,那么就会使用fd_install,得到该进程的flie_struct,以第一步得到的fd号,将file结构体赋值给该进程的fd_arrays[fd]。
ps.若文件打开数超过32,那么会重新分配一个fd_arrays,并更新max_fd
之后对该文件的所有read,write等操作,都会使用fd从fd_arrays中找到file结构体,然后执行file->f_ops执行驱动程序操做函数

解决方案:增大最大可打开文件数目

临时配置,退出shell复原

ulimit -HSn 100000000

more advanced method:https://www.dazhuanlan.com/2019/09/25/5d8b819277ece/

问题4.服务端作为客户端去发起请求,在请求结束后,会产生大量的TIME-WAIT连接。

为什么需要TIME-WAIT?
对客户端自己来说:TIME-WAIT等待2xMSL时间,能够保证所有已失效的报文段都消失,避免接受到来自服务端已失效的报文。

对服务端来说:TIME-WAIT等待2xMSL时间,能够保证服务端第三次FIN报文发送失败时,能够有多余时间重发FIN报文,能够从LAST_ACK进入到CLOSE状态。

解决方案:在产生大量TIME—WAIT的机器上,开启tcp_tw_reuse

TIME-WAIT多的机器开启tcp_tw_reuse和tcp_timestamps
其对应的服务端要开启tcp_timestamps
即:tcp_timestamps两端都需要开启,tcp_tw_reuse只在产生大量TIME-WAIT的机器上开启

在对应的机器上/etc/sysctl.conf文件中添加
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_timestamps =1

sudo vim /etc/sysctl.conf
sudo /sbin/sysctl -p

两方机器都开tcp_timestamps后,内核会比较数据包时间戳,由此对于客户端来说便可以保证一定不会收到失效的报文数据段。

对服务端来说,第三次FIN报文发送失败,仍处于LAST-ACK状态,但reuse会立即复用TIME-WAIT连接发送SYN包,去请求处于LAST-ACK状态的服务器时会返回FIN,ACK包,客户端再立即发送RST包置服务端close态后再次发送SYN包建立连接。
当服务器重发FIN,ACK包时,客户端仍处于连接建立状态,其sock既不是TCP_ESTABLISHED也不是TCP_LISTEN,那么最后内核就会执行到tcp_rcv_state_process中,若客户端处于TCP_SYN_SENT态,那么就会触发reset,重置tcp连接。

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{struct sock *rsk;//sockif (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */}//检查数据包是否完整if (tcp_checksum_complete(skb))goto csum_err;if (sk->sk_state == TCP_LISTEN) {} elsesock_rps_save_rxhash(sk, skb);//进行reset操做if (tcp_rcv_state_process(sk, skb)) {rsk = sk;goto reset;}return 0;

内核

sk_buff

不仅可以用双向链表的形式组织,还可以用红黑树树的形式组织。

struct sk_buff {union {struct {/* These two members must be first. */struct sk_buff      *next;struct sk_buff        *prev;union {ktime_t        tstamp;struct skb_mstamp skb_mstamp;};};struct rb_node  rbnode; /* used in netem & tcp stack */};........
}

双链表组织形式,当sk_receive_queue中的spinlock被占用时,将数据包存储到sk_backlog上,

struct sock{struct sk_buff_head  sk_receive_queue;struct {atomic_t   rmem_alloc;int      len;struct sk_buff  *head;struct sk_buff    *tail;} sk_backlog;
}


sk_buffer的实际存储空间由四个unsinged char*的指针决定。

struct sk_buff {sk_buff_data_t       tail;sk_buff_data_t     end;unsigned char       *head,*data;
}

tail - data是sk_buffer的实际存储空间。

为什么说每次从sock中读取数据都要从内核态内存拷贝到用户态内存?这是因为sk_buffer是通过调用__alloc_skb函数进行分配初始化的。在__alloc_skb中通过kmem_cache_alloc_node分配内核内存地址空间,所以sk_buffer中的四个指针均指向内核地址空间。

struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,int flags, int node)
{struct sk_buff *skb;skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node);data = kmalloc_reserve(size, gfp_mask, node, &pfmemalloc);skb->head = data;skb->data = data;
}

sock的获取

在tcp协议栈总入口函数tcp_v4_rcv中,使用__inet_lookup_skb来通过源端口和目的端口号来确定sock。如果找不到对应的sock那么将会直接goto no_tcp_socket

int tcp_v4_rcv(struct sk_buff *skb)
{....th = (const struct tcphdr *)skb->data;sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,th->dest, &refcounted);if (!sk)goto no_tcp_socket;....
}

在__inet_lookup_skb中会尝试直接从sk_buffer中直接获取sock,如果无法找到那么会使用ip层来确定sock

static inline struct sock *__inet_lookup_skb(struct inet_hashinfo *hashinfo,struct sk_buff *skb,int doff,const __be16 sport,const __be16 dport,bool *refcounted)
{struct sock *sk = skb_steal_sock(skb);//直接获取sockconst struct iphdr *iph = ip_hdr(skb);*refcounted = true;if (sk)return sk;return __inet_lookup(dev_net(skb_dst(skb)->dev), hashinfo, skb,doff, iph->saddr, sport,iph->daddr, dport, inet_iif(skb),refcounted);
}

在__inet_lookup中,会将sk_buffer转换为ip形式,然后传入(源IP,源端口,目的IP,目的端口),最后在__inet_lookup_established中,(源IP,源端口,目的IP,目的端口)会被hash为一个数,用于快速查找sock

struct sock *__inet_lookup_established(struct net *net,struct inet_hashinfo *hashinfo,const __be32 saddr, const __be16 sport,const __be32 daddr, const u16 hnum,const int dif)
{unsigned int hash = inet_ehashfn(net, daddr, hnum, saddr, sport);unsigned int slot = hash & hashinfo->ehash_mask;struct inet_ehash_bucket *head = &hashinfo->ehash[slot];
}

所有的sock都被统一保存在inet_hashinfo 中,其中ehash是一个struct inet_ehash_bucket型数组,链起来的一个hash。随后会使用sk_nulls_for_each_rcu从slot中遍历sock,直到找到这个sock为止才停下。

struct inet_hashinfo {struct inet_ehash_bucket   *ehash;spinlock_t           *ehash_locks;unsigned int           ehash_mask;unsigned int         ehash_locks_mask;
}

为什么要求双方都开启timestap

每个连接在初始化的时,都会调用tcp_connect_init函数,在其中会根据sysctl_tcp_timestamps 选项来确定tcp头的大小,如果没有开启sysctl_tcp_timestamps的话,那么tcp头大小将不会加上TCPOLEN_TSTAMP_ALIGNED长度。

static void tcp_connect_init(struct sock *sk)
{const struct dst_entry *dst = __sk_dst_get(sk);struct tcp_sock *tp = tcp_sk(sk);__u8 rcv_wscale;/* We'll fix this up when we get a response from the other end.* See tcp_input.c:tcp_rcv_state_process case TCP_SYN_SENT.*/tp->tcp_header_len = sizeof(struct tcphdr) +(sysctl_tcp_timestamps ? TCPOLEN_TSTAMP_ALIGNED : 0);
}

又因为才tcp_rcv_established中,会通过判断tcp头长度来判断是否需要检查timestamp

void tcp_rcv_established(struct sock *sk, struct sk_buff *skb,const struct tcphdr *th, unsigned int len)
{/* Check timestamp */if (tcp_header_len == sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) {}}

SYN包,在tcp_syn_options中会设置timestamp选项

static unsigned int tcp_syn_options(struct sock *sk, struct sk_buff *skb,struct tcp_out_options *opts,struct tcp_md5sig_key **md5)
{if (likely(sysctl_tcp_timestamps && !*md5)) {opts->options |= OPTION_TS;opts->tsval = tcp_skb_timestamp(skb) + tp->tsoffset;opts->tsecr = tp->rx_opt.ts_recent;remaining -= TCPOLEN_TSTAMP_ALIGNED;}
}

SYN+ACK包,在tcp_synack_options中会设置timestamp选项,只有当发现SYN包中有tstamp_ok选项时,才会开启设置timestamp选项。

static unsigned int tcp_synack_options(struct request_sock *req,unsigned int mss, struct sk_buff *skb,struct tcp_out_options *opts,const struct tcp_md5sig_key *md5,struct tcp_fastopen_cookie *foc)
{if (likely(ireq->tstamp_ok)) {opts->options |= OPTION_TS;opts->tsval = tcp_skb_timestamp(skb);opts->tsecr = req->ts_recent;remaining -= TCPOLEN_TSTAMP_ALIGNED;}
}

ts_recent:上一个sk_buffer时间,即上一个rcv_tsval的值
rcv_tsval: 当前sk_buffer时间
rcv_tsecr:回响时间

void tcp_rcv_established(struct sock *sk, struct sk_buff *skb,const struct tcphdr *th, unsigned int len)
{if ((s32)(tp->rx_opt.rcv_tsval - tp->rx_opt.ts_recent) < 0) //当前包的时间戳小于上次收到的包的时间戳goto slow_path; //并不意味着一定是旧包,需仔细检查
}

->tcp_transmit_skb()
->skb_mstamp_get() // 获取时间并写入sk_buffer
->tcp_syn_options() or tcp_established_options() // 将sk_buffer中的时间写入到tcp option中去

如果客户端未开启sysctl.sysctl_tcp_timestamps,那么在调用tcp_transmit_skb发送SYN报文时,在使用tcp_syn_options()构造TCP option字段时,将不会启动构造tcp_timestamps选项字段,这会使得在服务段收到SYN报文时,回送SYN+ACK时,在tcp_synack_options()也不会构造TCP option字段,因为synack会判断syn报文是否启用timestap功能。

如果服务端未开启sysctl.sysctl_tcp_timestamps,虽然在构造SYN+ACK报文时,会根据request_conn来构造一个带有timestamp的SYN+ACK报文回去,但是在最后tcp_connect_init()的时候却需要判断sysctl_tcp_timestamps来决定tp->tcp_header_len的长度,如果不开启sysctl_tcp_timestamps,那么将不会生成预期长度,tcp_header_len 是一个后续用于识别是否进行timestamp操做的重要选项。并且在tcp_parseOption时也需要判断sysctl_tcp_timestamps是否开启填充rcv_tsval和rcv_tsecr的值

static void tcp_connect_init(struct sock *sk)
{tp->tcp_header_len = sizeof(struct tcphdr) +(sysctl_tcp_timestamps ? TCPOLEN_TSTAMP_ALIGNED : 0);
}

万级并发服务器内核调优总结相关推荐

  1. linux7 kernel.sem,centos7.4内核调优,tcp单服务器万级并发

    在使用linux的centos7.4遇到的各种坑,其中一个项目采用四层架构,配置层,平台层,逻辑服务器管理层和集体逻辑服务器层的,一个整体的游戏项目,其中,作为整个项目负责人和架构打架着,项目运行一年 ...

  2. 家乐福618保卫战二-零售O2O场景中的万级并发交易情况下的极限性能调优

    本系列简介 这个系列可以帮助普通程序员们深刻的意识到平时工作中到底还有什么不足以及如何进一步进化成真正意义上的架构师.CTO以及后面的道路是如何走的: 这个系列可以帮助企业IT管理者深刻意识到,性能安 ...

  3. apache服务器性能不行,Apache服务器性能调优

    文章目录 [隐藏] 工具 Apache mod_status Apache2Buddy 多处理模块 Prefork Worker Event 模块配置 StartServers MinSpareSer ...

  4. 大战C100K之-Linux内核调优篇--转载

    原文地址:http://joyexpr.com/2013/11/22/c100k-4-kernel-tuning/ 早期的系统,系统资源包括CPU.内存等都是非常有限的,系统为了保持公平,默认要限制进 ...

  5. alsa 测试 linux_Linux低延迟服务器系统调优

    最近做了一些系统和网络调优相关的测试,达到了期望的效果,有些感悟.同时,我也发现知乎上对Linux服务器低延迟技术的讨论比较欠缺(满嘴高并发现象):或者对现今cpu + 网卡的低延迟潜力认识不足(动辄 ...

  6. 并发与性能调优(后续补充)

    高并发情况下,我们系统是如何支撑大量的请求的? 1.尽量使用缓存技术, 包括用户缓存,信息缓存还有静态页面缓存,多花点内存来做缓存,可以大大减少与数据库的交互次数和tomcat执行次数,减少不变的数据 ...

  7. linux内核调优参考

    对于新部署的机器,需要做一些基本的调优操作,以更改一些默认配置带来的性能问题 1 修改打开文件数 root@mysql:/data/tools/db# vim /etc/security/limits ...

  8. 20个Linux服务器性能调优技巧

    Linux高可用服务器集群解决方案让IT系统管理员可以从容应对许多常见的硬件和软件故障,允许多台计算机一起工作,为关键服务正常运行提供保障,系统管理员可以不中断服务执行维护和升级.Linux服务器有各 ...

  9. TCP百万并发服务器优化调参

    C语言TCP服务器百万并发调参优化 文章目录 C语言TCP服务器百万并发调参优化 背景 实验准备 优化调参 出现Connection refused错误 客户端连接服务器时候产生Too many op ...

最新文章

  1. python数据库pymysql_Python——数据库04 Python操作MySQL pymysql模块使用,python,04python,MySQLpymysql...
  2. ABC Perl Programing - 回 2gua 短消息
  3. PHP CURL 中文说明
  4. serv-u 数据备份_如何使用用户数据脚本在EC2实例上安装Apache Web Server
  5. Airflow 中文文档:快速开始
  6. mysql 执行时间有波动_阿里P8架构师谈mysql性能优化思路
  7. IO编程——文件复制操作
  8. 通信电子线路实验-调幅模块仿真(发送与接收)
  9. Unity游戏接入Steam成就
  10. 为什么苹果允许用户安装未受信任的企业级开发者所开发的软件?
  11. 通过思科模拟器配置思科2960交换机的办法:
  12. 声纹识别概述(3)声纹识别系统
  13. 用命令备份与还原网络设置
  14. 工作流模式的学习与总结
  15. 地图的电子围栏功能的技术选型
  16. python 随机森林参数说明
  17. 实现全托管,腾讯云服务网格的架构演进
  18. 慢慢买 | 比价API电商数据采集定制
  19. SQL练习——经典50题
  20. Android字数限制的EditText实现

热门文章

  1. python中plt.cm.Paired
  2. unity 数字键的输入及刚体的速度的设置
  3. python3报错: takes 1 positional argument but 2 were given 问题解决。
  4. 对大学三年学习生活的总结与反思
  5. Linux perf 1.4、hardware events
  6. Ubuntu中LightDM是什么(转)
  7. 推荐一些学习嵌入式经典的书籍
  8. win10安装mongoDB
  9. 【钉钉】通过钉钉机器人抓取群消息
  10. 新晋云计算工程师就业的感受和经验分享