# go-web-demo **Repository Path**: hoowaynew/go-web-demo ## Basic Information - **Project Name**: go-web-demo - **Description**: 使用go语言开发的demo级别web项目,项目技术栈包含MySQL,Redis,Gin框架以及简单的前端页面等 - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 3 - **Forks**: 1 - **Created**: 2021-10-10 - **Last Updated**: 2024-05-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README #
**Entry Task技术文档**
[toc] ## 一、背景及目的 EntryTask项目,通过实现RPC通用框架,使用Go HTTP API,MySQL或Redis操作等能力完成一个用户管理系统,主要功能包含用户(管理员)登录及用户数据的增删查改功能。 ![entry_task_login](./doc/pic/entry_task_login.png)
1.用户登陆页面
![entry_task_login_user_not_exist](./doc/pic/entry_task_login_user_not_exist.png)
2.用户登陆用户不存在
![entry_task_login_password_incorrect](./doc/pic/entry_task_login_password_incorrect.png)
3.用户登陆密码不正确
![entry_task_logon_info](./doc/pic/entry_task_logon_info.png)
4.用户登陆后信息展示
![entry_task_info_update](./doc/pic/entry_task_info_update.png)
4.用户信息变更后页面
## 二、逻辑架构设计 整体分为API层(RPC consumer)、TCP Server层(RPC provider)两层,用户上传的头像图片存放于磁盘static/pic/路径下,项目的具体代码结构组织如下。 ![entry_task_code_package](./doc/pic/entry_task_code_package.png) ``` html ├── README.md ├── main.go // 服务端程序入口 ├── conf │ ├── config.yaml // 配置文件,如web和rpc的启动地址和MySQL和Redis的参数配置 ├── pkg // 依赖框架代码包路径 │ ├── error // 自定义错误码代码包 │ │ ├── code.go // 错误码统一管理类 │ │ └── error.go // 自定义错误类型封装类 │ └── rpc // rpc框架包 │ ├── session.go // 网络回话调用实现类,用于从连接conn中读取和写入数据 │ ├── codec.go // 序列化编解码实现类 │ ├── server.go // rpc服务端框架实现类 │ ├── pooled_obj.go // rpc客户端线程池实现类(go-common-pool) │ ├── pooled_client.go // rpc客户端池化实现类 │ └── cleint.go // rpc客户端框架实现类 ├── rpc // rpc服务端业务实现核心包 │ ├── config │ │ ├── config.go // 配置类,用于启动加载MySQL和Redis等配置 │ ├── dal │ │ ├── model_dao.go // 原始crud的dao类 │ │ ├── user_manager.go // user操作数据库类 │ ├── manager │ │ ├── rpc_register.go // rpc接口注册类 │ └── service │ ├── user.go // 真正user服务类 │ └── cache.go // 缓存Redis操作类 ├── facade │ └── facade.go // rpc服务接口调用类 ├── go.mod ├── go.sum ├── handler │ ├── response_handler.go // 统一的web response处理器 │ └── user_service.go // api操作service类,如用户登陆,用户信息更新等 ├── log // 日志记录文件包路径 ├── model // 业务领域model包 │ └── user.go ├── router // api路由包 │ ├── middleware │ │ ├── middleware.go // gin框架http请求头设置类 │ │ └── rpc_provider.go // rpc接口配置及线程池配置类 │ └── router.go // api程序router路由 ├── static // 静态资源和图片路径 │ ├── pic // 头像图片上传路径 │ │ └── pic1.jpg │ ├── index.html // 登陆态用户信息落地页 │ └── login.html // 用户登陆页面 ├── test // 测试用例包路径 │ ├── rpc_test.go │ └── session_test │ └── util // 工具类路径 ``` ## **三、核心逻辑详细设计** ### 0、用例图 ![entry_task_case](./doc/pic/entry_task_case.png) ### 1、用户登录 ![entry_task_user_login_activity](./doc/pic/entry_task_user_login_activity.png) ### 2、登陆态用户信息回显 ![entry_task_user_echo_activity](./doc/pic/entry_task_user_echo_activity.png) ### 3、用户信息更新(nickname和头像) ![entry_task_user_update_activity](./doc/pic/entry_task_user_update_activity.png) ## **四、接口设计** *给出对外接口描述,包括名称、地址或URL、类型(GRPC/HTTPS等)、协议内容(PB/JSON等)、各参数(类型、描述、限制)等;* *对外接口需要给出鉴权机制和其他安全性考虑;* ### *1.创建新用户(http)* #### Reuqest - Method: **POST** - URL: *http://localhost:8181/user/token/login* - DESC:用户登陆接口(*用户登陆鉴权采用cookie-session机制,httpOnly设置为true防止前端js获取造成XSS攻击*) - Headers: Content-Type:multipart/form-data - Body: ```json { "username" : "hooker", "password" : "123456" } ``` #### Response - Body ```json { "code": 0, "message": "OK", "data": { "id": 0, "username": "hooker", "nickname": "hooker520", "profile": "./static/pic/1634291433769046000_pic2.jpg" } } ``` ### *2.用户态查询(http)* #### Reuqest - Method: **GET** - URL: **http://localhost:8181/user/token/query** - DESC:根据用户的token获取用户信息 - Headers: Content-Type:multipart/form-data;Cookie:token=E50E4F994DE9196A7CC3479621F96B82 - Body: ```json null ``` #### Response - Body ```json { "code": 0, "message": "OK", "data": { "id": 0, "username": "hooker", "nickname": "hooker520", "profile": "./static/pic/1634291433769046000_pic2.jpg" } } ``` ### *3.用户信息更新(http)* #### Reuqest - Method: **POST** - URL: **http://localhost:8181/user/token/update** - DESC:更新用户的nickname和profile接口 - Headers: Content-Type:multipart/form-data;Cookie:token=E50E4F994DE9196A7CC3479621F96B82 - Body: ```json { "username" : "hooker", "password" : "123456", "profile": "./static/pic.png" } ``` #### Response - Body ```json // 发起重定向跳回用户信息落地页面 context.Redirect(http.StatusMovedPermanently, "../static/index.html") ``` ### *4.用户信息查询(http)* #### Reuqest - Method: **GET** - URL: **http://localhost:8181/user/name/query** - DESC:根据用户名查询用户信息接口 - Headers: Content-Type:application/json - Body: ```json null ``` #### Response - Body ```json // 发起重定向跳回用户信息落地页面 { "code": 0, "message": "OK", "data": { "id": 0, "username": "hooker", "nickname": "hooker520", "profile": "./static/pic/1634291433769046000_pic2.jpg" } } ``` 4. *http://localhost:8181/user/add:用户添加接口 -暂未使用* ### *1.用户登陆(rpc)* #### Reuqest ```go method:var UserLogin func(request *UserLoginRequest) (user *User, token string, err *errno.Errno) // UserLoginRequest 用户登录时的请求对象 type UserLoginRequest struct { Username string `json:"username"` Password string `json:"password"` } ``` #### Response ```go // User 返回用户信息对象 type User struct { ID int64 `json:"id"` Username string `json:"username"` Nickname string `json:"nickname"` Profile string `json:"profile"` } // Errno user define error type Errno struct { Code int Message string } ``` ### *2.根据姓名查询用户信息(rpc)* #### Reuqest ```go method:var UserQueryByName func(username string) (user *User, err *errno.Errno) ``` #### Response ```go // User 返回用户信息对象 type User struct { ID int64 `json:"id"` Username string `json:"username"` Nickname string `json:"nickname"` Profile string `json:"profile"` } // Errno user define error type Errno struct { Code int Message string } ``` ### *3.根据token查询用户信息(rpc)* #### Reuqest ```go method:var UserQueryByToken func(token string) (user *User, err *errno.Errno) ``` #### Response ```go // User 返回用户信息对象 type User struct { ID int64 `json:"id"` Username string `json:"username"` Nickname string `json:"nickname"` Profile string `json:"profile"` } // Errno user define error type Errno struct { Code int Message string } ``` ### *4.更新用户信息(rpc)* #### Reuqest ```go method:var UserUpdateInfo func(request *UserUpdateRequest) (user *User, err *errno.Errno) // UserUpdateRequest 用户更新昵称和头像时的请求对象 type UserUpdateRequest struct { Username string `json:"username"` Nickname string `json:"nickname"` ProfilePath string `json:"profile"` } ``` #### Response ```go // User 返回用户信息对象 type User struct { ID int64 `json:"id"` Username string `json:"username"` Nickname string `json:"nickname"` Profile string `json:"profile"` } // Errno user define error type Errno struct { Code int Message string } ``` ## **五、存储设计** *可包括:* *1、用户信息t_user数据库表定义如下,username设置唯一索引;* ```sql CREATE TABLE `t_user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID', `username` varchar(64) NOT NULL DEFAULT '' COMMENT '用户名', `nickname` varchar(128) NOT NULL DEFAULT '' COMMENT '昵称', `password` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9' COMMENT '登录密码,默认UPPER(SHA1(UNHEX(SHA1(''123456''))))', `profile` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '/profile/default.jpg' COMMENT '用户头像图片地址', `state` tinyint NOT NULL DEFAULT '1' COMMENT '逻辑状态(1正常,2冻结,0已删除)', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最近更新时间', PRIMARY KEY (`id`), UNIQUE KEY `unique_idx_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=33 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户信息表' ``` *2、Redis缓存主要利用Hash结构缓存登陆用户信息以及部分string存取登陆的token;* ![redis_user_hash_info](./doc/pic/redis_user_hash_info.png) *3、当前用户上传的头像图片存储服务器磁盘,暂未搭建文件服务器等优化手段;* *1、数据库表定义、字段定义、索引、主/备库读写等;* *2、缓存KV设计、加载/更新/失效逻辑等;* *3、文件存储介质、目录组织形式、数据格式、索引、保存周期与清理机制等;* *4、消息队列等中间件选型;* ## **六、外部依赖与限制** *可包括:* *1、对公司内部跨产品系统或者外部的依赖,如SPM、CoreServer、银行、供应商等。* *2、受限于外部依赖的现有条件产生的限制,比如接口时延、TPS等;* 1、Mac下可食用Homebrew进行MySQL和Redis安装,会依赖于Homebrew软件* ```markdown 1.安装MySQL和Redis指令 - brew install mysql / brew install redis 2.启动MySQL和Redis指令 - brew services start|stop mysql / brew services start|stop redis ``` *2、MySQL和Redis也可使用Docker进行搭建(默认配置较低需更改配置),会依赖于Docker环境的配置及软件安装* ``` go 1. 首先安装Docker Desktop以及使用终端执行安装brew install docker(首先需要安装homebrew); 2. 然后可以使用docker命令安装MySQL和Redis等软件 // 安装和启动MySQL指令 - docker pull mysql:latest // 安装最新版本的MySQL,可指定版本安装 - docker images // 查看本地安装的docker软件镜像 - docker run -itd --name mysql-master -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql // 安装完成后,首次使用该命令来运行 MySQL 容器 - docker start/stop mysql-master // 后续使用该指令运行和关闭MySQL - docker ps // 查看当前容器的安装和运行情况 // 安装和启动Redis指令 - docker pull redis:latest // 安装最新版本的 redis,可指定版本安装 - docker run -itd --name redis-master -p 6379:6379 redis // -p 6379:6379:映射容器服务的 6379 端口到宿主机的 6379 端口。外部可以直接通过宿主机ip:6379 访问到 redis 的服务 - docker start/stop redis-master // 后续使用该指令运行和关闭Redis - docker exec -it redis-master /bin/bash // 然后可以通过 redis-cli 连接测试使用 redis 服务 // 其他软件安装类似,如 nginx - docker pull nginx:latest // 这里拉取官方的最新版本的镜像 - docker run --name nginx-master -p 8080:80 -d nginx // -p 8080:80: 端口进行映射,将本地 8080 端口映射到容器内部的 80 端口 ``` ## **七、部署方案与环境要求** *可包括:* *1、配置初始化、更改、下发、推送等;* *2、各种存储容量的预估、需要扩容的实例、备库、账号要求等;* *3、接入层、逻辑层实例数;* *4、VIP、域名、防火墙等特殊要求;* *1、当前核心配置存放在/conf/config.yaml中,包含MySQL和Redis的相关配置,RPC和Web地址配置等* 2、当前图片路径为了便于处理在代码中指定在static/pic/路径下存放 ## **八、项目SLA** *目标:* - 数据库必须有10,000,000条用户账号信息 - 必须确保返回结果是正确的 - 每个请求都要包含RPC调用以及Mysql或Redis访问 - 200并发(固定用户)情况下,HTTP API QPS大于3000 - - 200个client(200条TCP连接),每个client模拟一个用户(因此需要200个不同的固定用户账号) - 200并发(随机用户)情况下,HTTP API QPS大于1000 - - 200个client(200条TCP连接),每个client每次随机从10,000,000条记录中选取一个用户,发起请求(如果涉及到鉴权,可以使用一个测试用的token) - 2000并发(固定用户)情况下,HTTP API QPS大于1500 - 2000并发(随机用户)情况下,HTTP API QPS大于800 *1、可支持的存储容量;* *2、关键接口可支撑的TPS、QPS、时延等;* *3、系统可用性保证,如故障时间、恢复时间、Crash率等;* ### 1.并发固定200用户(清空redis缓存) ``` html ➜ test git:(master) ✗ go test -v -bench=Login -benchmem goos: darwin goarch: amd64 pkg: entrytask/test cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkLogin benchmark login parallelism:200 benchmark login parallelism:200 benchmark login parallelism:200 benchmark login parallelism:200 BenchmarkLogin-12 5770 222157 ns/op 6740 B/op 92 allocs/op PASS ok entrytask/test 2.712s ``` ### 2.并发固定200用户(存在redis) ```html ➜ test git:(master) ✗ go test -v -bench=Login -benchmem goos: darwin goarch: amd64 pkg: entrytask/test cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkLogin benchmark login parallelism:200 benchmark login parallelism:200 benchmark login parallelism:200 benchmark login parallelism:200 BenchmarkLogin-12 5108 227180 ns/op 6822 B/op 93 allocs/op PASS ok entrytask/test 1.865s ``` ### 3.并定固定2000用户(清空redis) ```html test git:(master) ✗ go test -v -bench=Login -benchmem goos: darwin goarch: amd64 pkg: entrytask/test cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkLogin benchmark login parallelism:2000 benchmark login parallelism:2000 benchmark login parallelism:2000 benchmark login parallelism:2000 BenchmarkLogin-12 3709 304117 ns/op 15223 B/op 140 allocs/op PASS ok entrytask/test 2.234s ``` ### 4.并发固定2000用户(存在redis) ```html ➜ test git:(master) ✗ go test -v -bench=Login -benchmem goos: darwin goarch: amd64 pkg: entrytask/test cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkLogin benchmark login parallelism:2000 benchmark login parallelism:2000 benchmark login parallelism:2000 BenchmarkLogin-12 4402 253539 ns/op 14200 B/op 133 allocs/op PASS ok entrytask/test 1.294s ``` ### 5.并发随机200用户 ```html ➜ test git:(master) ✗ go test -v -bench=QueryUser -benchmem -benchtime=5s goos: darwin goarch: amd64 pkg: entrytask/test cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkQueryUser benchmark query user parallelism:200 benchmark query user parallelism:200 benchmark query user parallelism:200 benchmark query user parallelism:200 BenchmarkQueryUser-12 51646 120465 ns/op 4479 B/op 62 allocs/op PASS ok entrytask/test 8.105s ``` ### 6.并发随机2000用户 ```html ➜ test git:(master) ✗ go test -v -bench=QueryUser -benchmem -benchtime=5s goos: darwin goarch: amd64 pkg: entrytask/test cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkQueryUser benchmark query user parallelism:2000 benchmark query user parallelism:2000 benchmark query user parallelism:2000 benchmark query user parallelism:2000 BenchmarkQueryUser-12 43384 135143 ns/op 5300 B/op 67 allocs/op PASS ok entrytask/test 7.854s ``` ### 7.jmeter压测结果截图 ![jmeter_200thred_100round_fixed](./doc/pic/jmeter_200thred_100round_fixed.png)
1.固定200用户100轮循环访问
![jmeter_200thred_100round_random](./doc/pic/jmeter_200thred_100round_random.png)
2.随机200用户100轮循环访问
![jmeter_2000thred_10round_fixed](./doc/pic/jmeter_2000thred_10round_fixed.png)
1.固定2000用户10轮循环访问
![jmeter_2000thred_10round_random](./doc/pic/jmeter_2000thred_10round_random.png)
1.随机2000用户10轮循环访问
### **8.jmeter压测结果分析:** | 测试项 | 固定200用户并发 | 随机200用户并发 | 固定2000用户并发 | 随机2000用户并发 | | :------: | :-------------: | :-------------: | :--------------: | :--------------: | | 平均耗时 | 10ms | 24ms | 217ms | 499ms | | 平均QPS | 6657.8(t/s) | 4318.7(t/s) | 5007.5(t/s) | 2830.5(t/s) | | 目标QPS | 3000 | 1000 | 1500 | 800 | ## **九、遗留问题与风险预估** *1、本次方案受限于时间、人力、外部因素等原因,未充分设计或者实现,代码可能存在潜在风险及Bug。* *2、受限于外部依赖限制、硬件资源、网络等不可控条件以及暂未引入网关、未使用Https传输,存在运行风险。* 3、当前用户的密码在数据库虽然做了不可逆加密,但是前端仍然是明文传递,这里可以进一步做好加密保护操作。 4.当前静态文件及网页文件与项目混在一起,后续可引入nginx代理甚至文件服务器托管静态文件上传。 5.当前性能压测粒度还比较粗糙,可以更细粒度的压测,挖掘更多潜在问题和优化点(Gin和RPC线程池数,编解码序列化,网络传输TCP连接管理,数据库连接数和Buffer Size,操作系统文件句柄限制等)。 ## **十、附录** *可包括一些附带的内容,例如引用的文档链接、提供的操作手册附件等* 1. Go语言及框架 2. Go Web编程 3. Go RPC 开发指南 4. [七天用Go从零实现系列](https://geektutu.com/post/gee.html) 5. 菜鸟Docker教程 6. Go语言基础Web项目框架