前言
9 月的时候,为了后面开发准备学习一些新玩意儿,按照我的习惯,学习新东西一般比较倾向于做一个 mini project ,结合自己的需求,于是选了这个 DDNS 分线路解析作为学习的 mini project
学到的新玩意儿
之前是用 ini 包做的解析,使用 viper 可以读取 JSON 、 TOML 、YAML、 INI 在内的多种格式的配置文件,甚至是 Java 的 properties 文件
通过 Go 的 embed 包,可以把所有文件(比如前端文件和静态资源)打包进一个二进制文件运行,方便部署的同时可以规避同时打开文件数量过多的问题(公司有个老项目,java 写的,没有打成一个 jar 包,访问量大了时候经常出问题)
Gin 可以在等待处理完当前的连接之后再关闭,这个机制叫做 Gracefully Shutdown ,需要用到 Go 的 channel
顾名思义,是“模板”,可以在不通过请求接口的情况下,拿到后端对象的数据,由于是在服务器端渲染的,客户端直接拿到的是渲染好的数据
和名字一样,是 orm ,在对象有大量属性的时候,手写 sql 显然是不现实的,加上公司生产环境有 pgsql 和 Oracle 两种数据库,不可能写两种 sql ,所以需要掌握 orm 的使用
之前给定时,但是写的多少有点问题,这次也一并探究一下应该怎么写
在已经用了 gin 的情况下再用 net/http
包和 VictoriaMetrics/metrics
结合的方法就不大好了,可以用 gin metrics 导出 metrics 给 Prometheus 监控
接口文档,各个语言都有自己的实现,和前端联调的时候很方便
实践
viper 配置文件
和 ini 不一样, 需要先去建个模型,而不是直接拿数据了,这里我们拿出 CocoaSyncer 的一点代码简单看下,由于我们是从 yaml 配置文件里面拿数据,所以需要建立这样的模型,后面的 json
、yaml
、 xorm
分别定义了 json 序列化,yaml 解析,xorm 建表时候的一些参数
读取配置文件的时候,定义配置文件的文件名、后缀、搜索路径,使用 viper.Unmarshal()
方法即可
type CocoaBasic struct { APIPort int `json:"apiPort" yaml:"APIPort" xorm:"'apiPort' comment('CocoaSyncer API 服务端口')"` NodeName string `json:"nodeName" yaml:"NodeName" xorm:"'nodeName' comment('CocoaSyncer 节点友好名称(WEB 展示及记忆用)')"` }
func readConfig(debugMode bool) { viper.SetConfigName("config") viper.SetConfigType("yaml") viper.AddConfigPath(".") if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { log.Fatalln("没有找到配置文件,请检查 ./config.yaml 是否存在!") } else { log.Fatalln("配置文件校验失败,请检查 ./config.yaml 是否存在语法错误") } } viper.Unmarshal(&CocoaBasic) }
|
gin 静态文件打包
Gracefully Shutdown
监听中断信号即可,请看以下代码片段
go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("listen: %s\n", err) }
}()
quit := make(chan os.Signal) signal.Notify(quit, os.Interrupt) <-quit log.Println("等待最长15秒处理完剩余连接后关闭服务")
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Fatal("服务器关闭超时,强制退出,原因:", err) } log.Println("服务成功关闭,mafumayumayumayu~~~~")
|
template
服务端渲染需要前后端一起完成
后端需要把 struct 转 map[string]interface{} ,在返回 html 的时候一并返回
func ShowStatus(context *gin.Context) { thisCocoa := &model.CocoaBasic{ConfigImported: true} subFunction.CocoaDataEngine.Where("configImported=true").Get(thisCocoa)
thisCocoa.DataBaseType = "*****" thisCocoa.Dsn = "*****" thisCocoa.CocoaSecret = "*****" thisCocoa.CloudPlatformInfo = nil thisCocoa.OtherCocoaSyncer = nil thisCocoa.CocoaManagedService = nil
thisCocoaJson, _ := json.Marshal(&thisCocoa) var thisCocoaMap map[string]interface{} _ = json.Unmarshal(thisCocoaJson, &thisCocoaMap)
context.HTML(http.StatusOK, "status.tmpl", thisCocoaMap) }
|
前端用花括号 .变量名
的方式来取
<title>CocoaSyncer - {{.nodeName}}</title> </head> <body style="background-image: url("/static/sakura.png");"> <div><center> <p> <span>CocoaSyncer - {{.nodeName}} 已正常运行<br>Powered By Luckykeeper < luckykeeper@luckykeeper.site | <a href="http://luckykeeper.site" target="_blank">http://luckykeeper.site</a> ></span> </p> <p> <span>本节点信息如下:</span> </p> <p> <table> <tr> <td class="CocoaProperties">数据最近一次更新时间</td> <td class="CocoaValues">{{.updatedAt}}</td> </tr> </table> </body>
|
效果可以看线上的这个例子:https://cocoasyncer.luckykeeper.site:44443/v1/status
xorm
通过 DSN 传连接信息,Oracle 需要取消引号,避免大小写导致的 SQL 问题,使用 Engine.Dialect().SetQuotePolicy(1)
即可(使用引号会限定大小写)
func InitializeDatabase(databaseType, dsnInfo string, debugMode bool) { var err error CocoaDataEngine, err = xorm.NewEngine(databaseType, dsnInfo) if err != nil { log.Fatalln("数据库初始化失败:", err) }
CocoaDataEngine.Dialect().SetQuotePolicy(1)
cacher := caches.NewLRUCacher(caches.NewMemoryStore(), 1000) CocoaDataEngine.SetDefaultCacher(cacher)
if debugMode { CocoaDataEngine.ShowSQL(true) } err = CocoaDataEngine.Ping() if err != nil { log.Panicln("数据库连接失败!检查数据库信息是否正确,程序返回原因为:", err) } else { CocoaDataEngine.SetConnMaxLifetime(time.Second * 60) CocoaDataEngine.SetMaxOpenConns(100) CocoaDataEngine.SetMaxIdleConns(3) log.Println("连接到远程数据库成功!") } }
|
cron
cron 的方法需要在 Gin 启动之前完成初始化,建议在 Init 阶段做这个事情
gin metrics
非常简单,首先在路由组添加一个 handler ,然后把业务数据一并导出即可
handler.PrometheusExporter(router)
func PrometheusExporter(router *gin.Engine) { monitor := ginmetrics.GetMonitor()
monitor.SetMetricPath("/metrics") monitor.SetSlowTime(5) monitor.SetDuration([]float64{0.1, 0.3, 1.2, 5, 10})
thisCocoa := model.CocoaBasic{ConfigImported: true} subFunction.CocoaDataEngine.Get(&thisCocoa)
cocoaSyncerStatus := &ginmetrics.Metric{ Type: ginmetrics.Gauge, Name: "cocoaSyncerStatus", Description: "cocoaSyncer运行状态", Labels: []string{"cocoaSyncerStatus"}, }
_ = ginmetrics.GetMonitor().AddMetric(cocoaSyncerStatus)
_ = ginmetrics.GetMonitor().GetMetric("cocoaSyncerStatus").SetGaugeValue([]string{"NodeNumber"}, float64(len(thisCocoa.OtherCocoaSyncer))) _ = ginmetrics.GetMonitor().GetMetric("cocoaSyncerStatus").SetGaugeValue([]string{"CocoaSyncerManagedService"}, float64(len(thisCocoa.CocoaManagedService))) _ = ginmetrics.GetMonitor().GetMetric("cocoaSyncerStatus").SetGaugeValue([]string{"CocoaSyncerCloudPlatformInfo"}, float64(len(thisCocoa.CloudPlatformInfo)))
nodeOnlineCount := 0 nodeOfflineCount := 0 for _, node := range thisCocoa.OtherCocoaSyncer { if node.StatusCode == 200 { nodeOnlineCount++ } else { nodeOfflineCount++ } }
_ = ginmetrics.GetMonitor().GetMetric("cocoaSyncerStatus").SetGaugeValue([]string{"CocoaSyncerNodeOnlineCount"}, float64(nodeOnlineCount)) _ = ginmetrics.GetMonitor().GetMetric("cocoaSyncerStatus").SetGaugeValue([]string{"CocoaSyncerNodeOfflineCount"}, float64(nodeOfflineCount))
monitor.Use(router) }
|
swagger
使用这个:swaggo/gin-swagger
初始化:swag -i /router/router.go
我是比较习惯在路由组初始化这个,然后把它直接写到路由里面
然后参考他的写法在 handler 的方法前面写上注释,生成文档即可
docs.SwaggerInfo.Title = "CocoaSyncer Swagger Doc Center" docs.SwaggerInfo.Description = "CocoaSyncer 在线文档 - V1" docs.SwaggerInfo.Version = "1.0" docs.SwaggerInfo.Host = strings.Split(thisCocoa.NodeAddress, "://")[1] docs.SwaggerInfo.BasePath = "/v1" docs.SwaggerInfo.Schemes = []string{"http", "https"}
if devMode { v1.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) }
|
业务逻辑
阿里云DNS提供了分线路解析的接口,只要把不同的线路解析到不同的 IP ,即可实现基于线路的 DDNS 和分流,同时节点之间做心跳,不通超过一段时间就认为是故障,正常的一方把本来是对端线路的解析改到自己头上即可。不过这样的场景很少,大家应该很少有这个需求,没有找到类似的项目,于是就自己做了一个
结语
通过这个 mini project ,学会了不少东西,可以说是给后面的 K8S 开发打下了基础,这两天我写的第一个 K8S 程序已经和前端大佬联调好了,已经发版上线啦,回头来给大家分享一下第一次 K8S 开发的一点体会