阻塞
#
阻塞(bio)指cpu等待io
非阻塞(nio)指调用io后立即返回,但要轮询事件状态
# 非阻塞指对cpu不阻塞,但业务线程阻塞
轮询(单线程)
read
定时重复调用来检查
select
前后read, 中间select轮询检查文件描述符的事件状态
采用1024长度数组存储状态,只能同时检查1024个文件描述符
poll
前后read, 中间poll
用链表代替数组, 也避免了不必要的检查
epoll # linux
前后read, 中间epoll
epoll检查不到事件,休眠epoll线程直到事件将它唤醒
kqueue # freeBSD中,类似epoll
aio # async io, linux, 业务线程不阻塞
通过回调(信号)传递数据,不必像epoll线程(业务线程)阻塞等待
仅linux下有, 只O_DIRECT方式读取,不能利用系统缓存
IOCP # windows aio
模拟aio(io线程池)
业务线程的io操作, 起io线程, io线程完成通信到业务线程触发回调
库
glibc(有bug)
libeio
node.js的libuv封装
linux下自实现
windows下IOCP
事件
#
实现
回调
队列存事件, 单进程检测事件是否回调
库
libevent
libev # bug比libevent少
工具
epoll(select, poll)
libev(libevent)
并发并行
#
并发
多任务共享时间段, 类比: 任务队列
为什么并发
多任务能力
非阻塞
并行
多任务同时处理, 类比: 多核处理器
为什么并行
提高执行效率
分类
任务并行化
数据并行化
cpu交替任务 # EDSAC串行任务
协作式 # 可能独占,Windows3.1, Mac OS 9
抢占式 # 任务管理器强制中断,Windows95, Mac OS 9以后版本, Unix, Linux
竞态条件
三条件
两个处理共享变量
一个修改中
另一个介入
没有共享
Multics(1969年)进程共享内存 # Multics基于PL/I和汇编编写
UNICS(1970年)进程不共享内存
UNIX10年后,线程共享进程内存
actor模型(1973年), 不共享内存,传递消息,异步 # Erlang, Scala
共享内存但不修改 # haskell所有变量,c++ const变量, scala val变量, java immutable(private属性没有setter)
不介入修改
线程协作式 # ruby的Fibre, python/js的generator
不便介入标志
锁(有线程不检查锁,还是可以进入) # 1965年提出,1974年改良为monitor。加解锁时,要求对锁的检查和修改同时执行
死锁问题
无法组合锁,组合要加新锁
事务内存 # 临时创建版本对其修改,更新失败重新执行
硬件事务内存(1986年硬件安装lisp的LM-2) # 1986年cpu MIPS基于RISC简化指令成功,LM-2商业失败
软件事务内存(1995年论文), 2005年微软concurrent haskell论文
2004年IBM X10, 2006年Sun Fortress, 2007年Clojure, 2010年微软终止.NET软件事务内存
并行代码
编译代码顺序不确定,或执行顺序不确定
看一句代码的内部实现, 在其中执行了行为
go func () { x = make([]int, 10) }()
x[9] = 1
业务并行解耦条件(满足幺半群性质)
封闭性 # 业务运算结果是业务
结合律 # 业务a、b的结果后与c执行,等同b、c的结果与a执行
单位元 # 恒等业务a与其它业务b执行,得b, 如reduce的初值
系统应用
并发能力
吞吐量(并行)
I/O多路复用(epoll)
cpu"多路复用"(进程、线程)
cpu机制(多发射、流水线、超标量、超线程)
进程线程应用
cpu对任务的M:N处理
进程切换处理任务
线程(通信,并行)
实现(异步, 并发,并行)
写法
回调(监听器), 链式(promise),同步(async)
事件处理器
调度方式: 单线程循环
协程
为什么用户实现协程
POSIX线程模型累赘
进程/线程 切换开销大
空间资源占用大
os调度对go模型不合理
go gc需要内存处理一致状态(所有线程停止), os调度时,因gc时间不确定,期间大量线程停止工作
# go调度器知道什么时候内存处于一致性状态(只需正在核上运行线程)
本质
用户态,寄存器+栈, 让出(协作而非抢占)
调度方式(线程模型)
N:1 # N个用户空间线程运行在1个内核空间线程
上下文切换快
无法利用多核
1:1
# POSIX(pthread), java
利用多核
上下文切换慢,每次调度都在用户态和内核态间切换
M:N
任意内核模型管理任意goroutine
调度复杂性大
go
M(machine)代表内核线程
G(goroutine)有自己的栈,程序计数器,调度信息(如正阻塞的channel)
P(processor)调度上下文, $GOMAXPROCS设置数量
P中有G队列(runqueue, 队尾添加新G)
当前运行一个G, 到调度点时,队列弹出另一个G
P周期检查全局G队列防止其中G饿死
P运行完,全局G队列拉取G
P运行完,全局G队列空,从其它P拉取一半G
P运行在M, M阻塞时P移到其它M, 阻塞M中保留阻塞的G
调度器创建足够多M跑P
阻塞M中G的syscall返回, M尝试偷一个P
没得到P时, 它的G加入全局G队列, M进线池睡眠
概念
过度竞争
过多线程尝试同时使用一个共享资源
同步 # 直接相互制约
实现
同步原语(如通道、锁)作用时,会刷处理器缓存到内存并提交,保证可见性
互斥 # 间接相互制约
竞态条件(race condition)
临界区 # 只能一线程访问的代码,如lock了的代码
监控模式 # 互斥锁, 函数, 变量 组合出临界区的模式, 使用了代理人(broker)(指锁)
异步
# 与同步相对。多线程是实现异步的一种手段
可见性
线程总可见到最后修改的数据, 脏读是反例
原子性
查看和修改同时发生
乱序执行
# java 中标记volatile的变量可以不乱序执行, 现多用原子变量
编译器或JVM的静态优化可以打乱代码执行顺序(java)
硬件可以通过乱序执行来优化性
死锁 # 多线程竞争资源而互相等待
条件
互斥 # 资源排他
不剥夺 # 资源不被外力剥夺
请求和保持条件 # 线程已保持一个资源,请求新资源。请求被阻塞而自己资源保持
循环等待 # 阻塞线程形成环
方案
锁按顺序获得 # a,b,c锁,要得c手中要有a, b
# 使用锁的地方比较零散时,遵守此顺序变得不实际
# 可以用对象散列值作全局顺序减小死锁机率
阻塞加时限
# 外星方法中可能包含另一把锁,要避免在持锁时调用外星方法
活锁 # 多线程尝试绕开死锁而过分同步反复冲突
多线程
#
线程池
作用
重复利用, 降低资源消耗
提高响应速度,不等线程创建
可管理,线程是稀缺资源,统一分配,调优和监控,提高系统稳定性
锁
公平锁 # FIFO取锁
非公平锁 # 每次直接占有
互斥锁(mutex) # 访问前加锁,访问后解锁
悲观锁 # 假设最坏,等所有线程释放成功
读加锁
乐观锁 # 假设最好,有冲突时重试
读不加锁,写时判断数据版本是否修改,再重试
读写锁(rwlock) # 竞争不激烈比互斥锁慢
读锁(共享锁)
写锁(互斥锁)
状态
读加锁状态
可多个线程占用
处理器缓存提交,数据可见
阻塞写线程 # 导致写线程抢占不到资源,所以有写线程时,阻塞后进入的读线程
写加锁状态
一次只有一个线程占用
阻塞所有线程
不加锁状态
自旋锁 spinlock
互斥锁改,自己进入循环等待状态(忙等) # 适合锁持有时间较短
RCU锁 Read-Copy Update
读写锁改,一个写线程,读线程无限制
实现垃圾回收器
写线程copy副本修改,向垃圾回收器注册callback以执行真正的修改
垃圾回收器收到信号,所有读线程结束,执行callback
可重入锁 # 互斥锁改,允许同一线程多次获得写锁
管程(monitor)
临界区(critical section)
内置锁、显示锁 # 指java的synchronized与Reentrantlock
信号量
进程, 线程间通知状态
CAS
#
# compare and swap,无锁算法(lock free), 非阻塞(non-blocking), 构成基本的乐观锁
# cpu实现的指令
3个操作数
# V的值为A时,原子更新成B,否则无操作。返回V的值
需要读写的内存位置V
进行比较的值A
拟写入的新值B
函数式
#
介绍
消除可变状态
概念
命令式语言中,求值顺序与源码的语句顺序紧密相关(有可能乱序执行)
函数式程序并不描述"如何求值以得到结果",而是描述"结果应当是什么样的"。函数式编程中,如何安排求值顺序相对自由
引用透明性
# 任何调用函数的地方,都可以用函数运行结果来替换函数调用,而不会产生副作用
数据流式编程(dataflow programming)
# (+ (+ 1 2) (+ 3 4))就是一个数据流,所有函数都可以用时执行
future模型
分离标识与状态
#
介绍
Clojure, 指令式编程和函数式编程混搭
clojure四种并发模型
vars (thread-local)
atoms原子变量
agent代理
refs引用 与 ATM软件事务内存
actor模型
#
介绍
作为actor自己修改自己的数据,对外提供消息,处理对外消息
共享内存模型和分布式内存模型,适合解决地理分布型问题,强大的容错性
基于消息传递,侧重通道两端实体
每个actor有一个mailbox, mailbox中转消息
csp
#
介绍
通信顺序进程(communicating sequential processes)
基于消息传递,侧重信息通道
数据级并行
#
# 不可变数据, 观测不可变、实现不可变
lambda架构
#
介绍
综合MapReduce和流式处理的特点,处理大数据问题的架构
状态保持
#
cookie
分域名, 客户端保存服务器定义数据, 请求时发送
session
服务器id数据,id下发到客户端
共享
# 同时多方案,动态切换 zookeeper切换环境变量与重启
# java中filter重写request getSession
webSphere或JBoss可配置session复制或共享
# 不好扩展和移植
加密存cookie
服务
redis
memorycache
gemfire # 12306
认证
#
单点登录
sessionID存cookie, cookie禁用存头域
token
类型
access token
# 标识唯一用户
user_id
issue_time
# token发放时间,单位秒
ttl
# 有效时间,uint16,单位分钟
mask
# int128, 按bit分组用户,用于批量封禁或其它功能
refresh token
# 用来换access token,与access token同时发放
# 过期时间更长
实现
redis存储
token不要太长
常见问题
#
CSRF
#
介绍
跨域请求伪造(cross-site request forgery)
client登录A, 本地生成cookie
client登录B, B给执行js,带参数请求站点A
解决
token验证 # 加入自定义头域
验证Referer头域
XSS
#
介绍
跨站脚本攻击(cross-site scripting, 易和css混淆,所以写成XSS), 渲染页面时脚本未转义
XSF
#
介绍
跨站flash攻击(cross-site flash), actionScript加载第三方flash
sql注入
#
介绍
拼装sql,参数插入sql逻辑
解决
sql预编译
1+N查询
#
介绍
先查出外键id集合, 再逐条id查关联表。orm易出的问题
解决
用 id IN (1,2,3)
commit前自动合并sql
业务场景
#
关注/信箱
#
要求
user人数10w, 活跃1w。
大部分user关注1k人, 一部分大v被关注100w人。
每人每天发100条博文
user新博文数量提醒,消息标记已读
表
user
user_followers
user_followed
user_posts(u_id, created_ts)
user_messages(u_id, p_id, is_read)
# 10w * 100条数据 / 天
定时任务拉取
user_followed拉u_id, user_posts表按时段拉id, 更新user_messages
优点
平均, 少次, 增量。
缺点
及时性中
每次对所有用户操作
数据
10w*1k*100条数据 / 天
发布时推送
有p_id, user_followers, 更新user_messages
优点
及时性高
缺点
计算集中, 可能高峰
数据
最高 100w*100条数据 / 次
10w*100次 / 天
messages处理
存部分messages
不活跃user不存message
在登录状态,定时拉取
优点
减少message
缺点
计算集中
数据
1k * N(N<100)条 / 次
1w * 1k * 100条数据 / 天
messages结构变化
u_id: [{p_id: uint, is_read: bool}] # 条数稳定为10w
用mongodb或redis
消息队列?
服务端存message状态,不能mq
如果客户端存状态,这就是个简单的mq问题
轻应用架构
#
node.js + mongodb
mysql
数据
#
数据迁移
#
去掉约束
排序(中断继续)
数据存储
#
缓存
queue + map
# queue存储、限量, map查询,指向queue中元素
缓存
#
queue + map
# queue存储、限量, map查询,指向queue中元素
实时并发
#
异步方案
#
node.js + mongodb
tornado + celery + rabbitmq + 优先级
quartz
消息
#
功能
好友
单聊, 群聊
语音, 视频
im # 浏览器聊天(tcp, 不https)
协议
XMPP # 基于xml
MQTT # 简单,但自己实现好友、群组
SIP # 复杂
私有协议 # 工作量大,扩展性差
go高并发实时消息推送
#
问题
长连接 # 支持多种协议(http、tcp)
server push
HTTP long polling(keep-alive)
基于TCP自定义
心跳侦测
高并发 #>= 10,000,000
C1000K
多种发送方式
单播: 点对点聊天
多播: 定点推送
广播: 全网推送
持久/非持久
准实时 # 200ms ~ 2s
gc卡顿是大问题
客户端多样性
同帐号多端接入
网络变化
电信、联通切换
wifi, 4g, 3g
断线、重连、断线、重连
系统架构
组件
room
# 接入客户端
分布式全对称
一个client一个goroutine
每个server一个channel存消息队列
book记录user与server映射
统一http server收消息并将消息路由到room和server
manager掌控room的服务:内部单播、多播、广播
admin负责room进程管理
center
# 运营人员从后台接入
提供操纵接口给应用服务器调用
restful
长时操作,有任务概念来管理
提供统计接口
register
# room和center注册
key-value的map,value是struct
记录用户连到哪个room
记录在线时长等信息
hash算法定位register进程
不直接用redis是为了添加业务逻辑
saver
# room和center调用
# 使用redis
分布式全对称
提供存储接口
采用encoding/gob编码格式的rpc
idgenerator
# saver和center调用
全局消息id生成器, int64
分布式,每个进程负责一块id区域
后台goroutine每隔一秒写一次磁盘,记录当前id
启动时跳过一段id,防止一秒内未写入磁盘的id重复生成
存储
redis
存核心数据
db_users: zset, 存各产品用户集合
db_slots: list, 存用户离线消息队列
db_buckets: dict, 存消息id -> 消息体
数据
16机器,标配24硬件线程, 64g内存
linux kernel 2.6.32 x86_64
单机80万并发连接
load 0.2 ~ 0.4 cpu
总使用率7%~10%
内存占用20g
目前接入1280万在线用户
2分钟一次gc, 停顿2秒,tip上提供了并行gc
15亿个心跳包/天
持续运行一个月无异常
直播
#
《关于直播,所有的技术细节都在这里了》
游戏
#
进程
gateway进程组
# 对外api
function进程组
# 注册玩家全局信息
session进程组
# 玩家状态
dbserver进程组
# 数据
多word进程组
# 不同地图的信息、逻辑