前言
此项目主要用于学习go语言,并且了解后台相关开发技术栈,比如rpc,缓存redis,数据库mysql等
项目github地址: usermana
功能需求
实现一个用户管理系统,用户可以登录、拉取和编辑他们的profiles。
用户可以通过在Web页面输入username和password登录,backend系统负责校验用户身份。
成功登录后,页面需要展示用户的相关信息;否则页面展示相关错误。
成功登录后,用户可以编辑以下内容:
上传profile picture
修改nickname(需要支持Unicode字符集,utf-8编码)
用户信息包括:
username(不可更改)
nickname
profile picture
需要提前将初始用户数据插入数据库用于测试。确保测试数据库中包含10,000,000条用户账号信息。
设计要求
- 分别实现HTTP server和TCP server,主要的功能逻辑放在TCP server实现
- Backend鉴权逻辑需要在TCP server实现
- 用户账号信息必须存储在MySQL数据库。通过MySQL Go client连接数据库
- 使用基于Auth/Session Token的鉴权机制,Token存储在redis,避免使用JWT等加密的形式。
- TCP server需要提供RPC API,RPC机制希望自己设计实现
- Web server不允许直连MySQL、Redis。所有HTTP请求只处理API和用户输入,具体的功能逻辑和数据库操作,需要通过RPC请求TCP server完成
- 尽可能使用Go标准库
- 安全性
- 鲁棒性
- 性能
开发环境
- 操作系统:macOS Catalina 10.15.6
- Go:1.15.6
- Mysql: 8.0.22
- Redis: 6.0.10
设计简介
此项目主要包含三个部分
- 用户登陆界面
- http server的服务
- tcp server的服务
Note:主要是http erver和tcp server模块,其中最重要的是tcp server。
HTTP Server负责处理HTTP请求,对用户数据进行简单处理并转发至TCP服务。
TCP Server处理HTTP服务转发的请求并访问MySQL和redis。
http server和tcp server 之间的通信主要是通过rpc协议。
rpc通信协议实现。
- rpc客户端:通过tcp 连接rpc服务器,发送不同的数据信息,从而调用不同的rpc服务。
- rpc服务端:首先注册一些服务(本质服务对应的服务处理函数), 监听请求。 当收到rpc客户端数据时,解析信息调用对应的服务并且将处理完成的结果返回给客户端。
实现流程
整体流程图
具体登陆获取信息请求
API接口
1.注册接口信息
URL | 方法 |
---|---|
http://localhost:1088/signUp | POST |
输入参数
参数名 | 描述 | 可选 |
---|---|---|
username | 用户名 | 否 |
password | 密码 | 否 |
nickname | 昵称 | 是 |
2.登录接口信息
URL | 方法 |
---|---|
http://localhost:1088/login | POST |
输入参数
参数名 | 描述 | 可选 |
---|---|---|
username | 用户名 | 否 |
password | 密码 | 否 |
3.获取用户信息接口信息
需要在登录接口之后调用
URL | 方法 |
---|---|
http://localhost:1088/profile | GET |
输入参数
参数名 | 描述 | 可选 |
---|---|---|
username | 用户名 | 否 |
4.更改用户昵称接口信息
需要在登录接口之后调用
URL | 方法 |
---|---|
http://localhost:1088/updateNickName | POST |
输入参数
参数名 | 描述 | 可选 |
---|---|---|
username | 用户名 | 否 |
nickname | 新昵称 | 否 |
5.更改用户头像接口信息
需要在登录接口之后调用
URL | 方法 |
---|---|
http://localhost:1088/uploadProfilePic | POST |
输入参数
参数名 | 描述 | 可选 |
---|---|---|
username | 用户名 | 否 |
image | 头像图片路径 | 否 |
数据储存
mysql设计
主要维护两张表,一张保存用户信息, 一张保存用户登陆信息。
更详细可以参考mysql/usermane.sql文件创建表格sql语句
用户信息表
Field | Field | Null | Key | Default | Extra |
---|---|---|---|---|---|
id | bigint | NO | PRI | NULL | auto_increment |
user_name | varchar(255) | NO | UNI | ||
nick_name | varchar(255) | NO | |||
pic_name | varchar(255) | YES |
note: pic_name字段主要用于存储用户头像的路径。
用户登陆信息表
Field | Field | Null | Key | Default | Extra |
---|---|---|---|---|---|
id | bigint | NO | PRI | NULL | auto_increment |
user_name | varchar(255) | NO | UNI | ||
password | varchar(255) | NO |
redis设计
redis缓存数据设计。
主要是缓冲登陆校验的token和用户信息,其中用户信息键值对中的值,是一个哈希表,表中有三项元素,分表是代表用户信息是否有效,用的的nick_name, 用户的pic_name。
key | value |
---|---|
auto_username | Token |
username | { [valid, 1/“”],[nick_name, “”] [pic_name,“”]} |
代码结构
1 | usermana |
部署
- 在conf/bench_conf配置系统资源
- 在redis/config.go配置redis和mysql
- 运行TCP server
1 | cd tcpServer |
- 运行HTTP server
1 | cd httpServer |
功能测试
用户登陆
用户登陆成功
显示用户信息
默认用户图像信息为空
修改用户头像路径
单元测试
简单对redis,mysql,tcpserver接口进行单元测试.
分别在redis, mysql, tcpServer,目录下执行
1 | go test |
主要执行redis/redis_test.go,mysql/mysql_test.go,tcpServer/tcpServer_test.go测试文件.
压力测试
模拟
使用程序模拟多用户并发请求,一个协程(goroutine)对应一个用户,并发启动多个协程,即可达到模拟测试目的。
运行benchmark/benchmark.go文件,即可模拟压力测试。
login:
固定用户 200 并发
Total Requests(50000) - Concurrency(200) - Random(false) - Cost(2.785033353s) - QPS(17954/sec)
随机用户 200 并发
Total Requests(50000) - Concurrency(200) - Random(true) - Cost(2.934093639s) - QPS(17042/sec)
固定用户 2000 并发
Total Requests(50000) - Concurrency(2000) - Random(false) - Cost(3.485402234s) - QPS(14346/sec)
随机用户 2000 并发
Total Requests(50000) - Concurrency(2000) - Random(true) - Cost(4.493010268s) - QPS(11129/sec)
profile:
固定用户 200 并发
Total Requests(50000) - Concurrency(200) - Random(false) - Cost(2.492745575s) - QPS(20059/sec)
随机用户 200 并发
Total Requests(50000) - Concurrency(200) - Random(true) - Cost(4.052824896s) - QPS(12338/sec)
固定用户 2000 并发
Total Requests(50000) - Concurrency(2000) - Random(false) - Cost(3.070350174s) - QPS(16285/sec)
随机用户 2000 并发
Total Requests(50000) - Concurrency(2000) - Random(true) - Cost(4.63655562s) - QPS(10784/sec)
updateNickName:
固定用户 200 并发
Total Requests(50000) - Concurrency(200) - Random(false) - Cost(3.166650838s) - QPS(15790/sec)
随机用户 200 并发
Total Requests(50000) - Concurrency(200) - Random(true) - Cost(3.303067683s) - QPS(15138/sec)
固定用户 2000 并发
Total Requests(50000) - Concurrency(2000) - Random(false) - Cost(3.834577733s) - QPS(13040/sec)
随机用户 2000 并发
Total Requests(50000) - Concurrency(2000) - Random(true) - Cost(3.893069542s) - QPS(12844/sec)
问题总结
这边整理一下遇到的问题,以及需要注意的点。
使用mysql/mysql_test.go去初始化数据库,插入1 000 000条数据,这个过程比较久,我在本机测试,需要90分钟。而go test默认最多运行10分钟,当运行大于10分钟,程序自动退出。因此为了顺利完整插入数据,可以设置timeout参数,指定go test运行完timeout时间后才退出。
1
go test -timeout=120m
表示go test运行时,直到完成所有测试或者到达120分钟, 程序才会退出。
在高并发多连接的情况下,还需要设置相关参数,比如一个进程最多允许打开的连接数,文件描述符等,还有mysql数据库连接池相关参数的设置。
- kern.ipc.somaxconn:表示内核最多并发连接数。默认为128,推荐在1024-4096之间,数字越大占用内存也越大。
1
sudo sysctl -w kern.ipc.somaxconn=2048
- kern.maxfilesperproc:每个进程能够同时打开的最大文件数量(根据需要灵活配置)
1
sudo sysctl -w kern.maxfiles=65536
- kern.maxfiles: 系统中允许的最多文件数量 (根据需要灵活配置)
1
sudo sysctl -w kern.maxfiles=22288
- ulimit -n: 打开的文件描述符(本系统默认2560), 这个设置受到maxfilesperproc和maxfiles的约束,不能大于其中任何一个。
1
ulimit -n 10000
Note: 每个控制台都需要独立设置才生效。
- max_connections:数据库最多连接数, 默认是150。
通过以下sql语句查看max_connections值。
1
show variables like "max_connections";
通过一下sql语句可以设置(根据需要灵活配置)
1
set global max_connections = 2000;
当遇到连接数超过max_connections时候,会报类似如下错误:
1
err:"Error 1040: Too many connections"
- mysql数据库初始化一些连接配置
1
2
3
4
5
6
7
8//设置mysql每个连接的生命周期,默认值0,即连接不关闭一直存在。
//此值的设置应该小于wait_timeout,避免连接因为超时而意外关闭。wait_timeout的默认值28800,8个小时,
//ConnMaxLifetime值推荐为wait_timeout的一半,即14400,4个小时。
db.SetConnMaxLifetime(config.ConnMaxLifetime)
//MaxIdleConns 连接池中最大空闲连接数.
db.SetMaxIdleConns(config.MaxIdleConns)
// MaxOpenConns 同时连接数据库中最多连接数. MaxIdleConns和MaxOpenConns最好设置一样
db.SetMaxOpenConns(config.MaxOpenConns)重点是back_log参数
此参数影响mysql server中tcp accept队列中的连接数。 当tcp accetp队列容量超过back_log时候,那么mysql server将会丢弃此连接,从而导致tcp 协议栈回应一个rst包给客户端。(back_log默认值151)
客户端收到rst之后,表现类似如下:
1
read tcp 127.0.0.1:55718->127.0.0.1:3306: read: connection reset by peer
然而,在go-sql-driver/mysql中,客户端收到了rst包,还会继续重试maxBadConnRetries(默认2)次连接。
因此,客户端还是有很大几率连接成功并且完成请求,除非maxBadConnRetries次连接都收到了rst包。
不过,这对于整体请求来说,响应就变慢了。换言之,QPS会大大减少。
因此,我们可以修改tcp accetp队列容量,使之比mysql的连接池容量还要大,就可以避免上述问题,从而提高QPS。
tcp acctpt队列容量除了受到back_log制约,还受到系统参数kern.ipc.somaxconn的制约。
换言之,tcp acctpt队列容量 = min(back_log, kern.ipc.somaxconn)。
back_log参数具体介绍可参考:back_log
back_log变量通过如下方式查看
1
show global variables like '%back_log%';
由于back_log是静态变量,因此通过在my.cnf文件修改,如下
1
2[mysqld]
back_log = 1000Note: 这个主要是排查花了1天多的时间,还是没有找到问题的本质所在,最后是靠Zhang Weiwei大佬解决的。
具体可以参考:MySQL出现connection reset by peer问题排查
排查问题的思路也是非常值得学习。
参考
- Go: http://golang.org
- Coding style: https://github.com/golang/go/wiki/CodeReviewComments
- Testing: https://golang.org/pkg/testing/
- Profiling: http://blog.golang.org/profiling-go-programs
- Go Web application example: https://golang.org/doc/articles/wiki/
- editor/IDE https://github.com/fatih/vim-go
- MySQL client: https://github.com/go-sql-driver/mysql
- Redis: http://redis.io
- Redis Client: https://github.com/go-redis/redis