了迹奇有没 2024-03-27T11:43:05+08:00 https://whrss.com/ whrss gorm 中 MySQL 错误码映射与主键冲突错误处理 gorm-mysql-error-handling-duplicate-key-translations 2024-03-27T11:43:05+08:00 2024-03-27T11:43:05+08:00

处理 gorm 错误返回时,有一些错误是没有办法直接使用 errors.Is 来进行判断的,比如主键冲突的错误,直接使用 errors.Is(err, gorm.ErrDuplicatedKey) 是无法判断出主键冲突的错误返回的。

如果没有办法进行判断,为什么 gorm 要给这样一个 error ,但又不能使用呢?

gorm.io/driver/mysql 包中有一个 error_translator 的 go 文件


package mysql



import (

    "github.com/go-sql-driver/mysql"



    "gorm.io/gorm")



// The error codes to map mysql errors to gorm errors, here is the mysql error codes reference https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html.

var errCodes = map[uint16]error{

    1062: gorm.ErrDuplicatedKey,

    1451: gorm.ErrForeignKeyViolated,

    1452: gorm.ErrForeignKeyViolated,

}



func (dialector Dialector) Translate(err error) error {

    if mysqlErr, ok := err.(*mysql.MySQLError); ok {

       if translatedErr, found := errCodes[mysqlErr.Number]; found {

          return translatedErr

       }

       return mysqlErr

    }



    return err

}

我们可以看到这个文件将 mysql 的几种错误码进行了枚举,使用 Translate 函数会将对应的 mysql error 转化为 gorm error

那这里的 Translate 函数,是谁进行使用了呢?在什么时候进行使用了呢?

主键冲突的错误一定是出现在插入的时候,我们顺着 gormCreate 方法向下找,可以发现它调用了一个 AddError 的函数,如下


// AddError add error to dbfunc (db *DB) AddError(err error) error {

    if err != nil {

       if db.Config.TranslateError {

          if errTranslator, ok := db.Dialector.(ErrorTranslator); ok {

             err = errTranslator.Translate(err)

          }

       }



       if db.Error == nil {

          db.Error = err

       } else {

          db.Error = fmt.Errorf("%v; %w", db.Error, err)

       }

    }

    return db.Error

}

这里有一行很关键,db.Dialector.(ErrorTranslator)Dialector 接口进行了断言,断言成功,就调用对应 DialectorTranslate 函数,而当这里的 Dialector 是上面 gorm.io/driver/mysql 中的 Dialector 时,就可以运行上面的翻译逻辑,将 mysql 的 error 转换为 gorm 的 error。

那么,我们下一步就是找到方法,把这里使用的 Dialector 替换成 gorm.io/driver/mysql 包下的这个 Dialector,这样我们就可以使用 errors.Is(err, gorm.ErrDuplicatedKey) 对插入冲突进行判断了。

gorm 中 DB 对象的结构是这样的


// DB GORM DB definition

type DB struct {

    *Config

    Error        error

    RowsAffected int64

    Statement    *Statement

    clone        int

}

这里的 Config 中就包含了 Dialector 接口,我们只需要在创建 gorm.DB 的时候,将接口的实例(gorm.io/driver/mysql 包下的这个)传入进去,就可以让 gorm 在之后的 error 判断时,对 mysql 的 error 进行翻译。

到这儿,原理部分就明明白白了,接下来简单改写一下 gorm.DB 的 init 过程即可!⬇️


import "gorm.io/driver/mysql"

import "gorm.io/gorm"



connUrl := "数据库连接地址"

db, err := gorm.Open(

mysql.Open(connUrl).(*mysql.Dialector),

TranslateError: true, // 开启 mysql 方言翻译 (开启后 duplicatedKey err 判断才能生效)

)

以上!

参考链接:https://gorm.io/docs/error_handling.html#Dialect-Translated-Errors

]]>
在使用 gorm 处理数据库操作时,尤其是针对 MySQL,有时我们会遇到 golang 标准库`errors.Is`函数无法直接识别特定的 gorm 错误类型的情况,如主键冲突错误。尽管 gorm 提供了`gorm.ErrDuplicatedKey`来表示此类错误,但在原始错误返回中并不能直接通过`errors.Is(err, gorm.ErrDuplicatedKey)`来进行判断。本文深入探究 gorm.io/driver/mysql 包中的错误转换机制,揭示了如何借助`error_translator`模块将 MySQL 的错误码映射为 gorm 的错误类型。
CloudFlare Tunnel 免费内网穿透的简明教程 cloudflare-tunnel 2024-02-27T13:01:35+08:00 2024-02-27T13:01:35+08:00

Tunnel 可以做什么

  • 将本地网络的服务暴露到公网,可以理解为内网穿透。 例如我们在本地服务器 localhost:8091 搭建了一个 博客网站,我们只能在内网环境才能访问这个服务,但通过内网穿透技术,我们可以在任何广域网环境下访问该服务。相比 NPS 之类传统穿透服务,Tunnel 不需要公网云服务器,同时自带域名解析,无需 DDNS 和公网 IP。

  • 将非常规端口服务转发到 80443 常规端口。 无论是使用公网 IP + DDNS 还是传统内网穿透服务,都免不了使用非常规端口进行访问,如果某些服务使用了复杂的重定向可能会导致 URL 中端口号丢失而引起不可控的问题,同时也不够优雅。

  • 自动为你的域名提供 HTTPS 认证。

  • 为你的服务提供额外保护认证。

  • 最重要的是——免费。

Tunnel 工作原理

Tunnel 通过在本地网络运行的一个 Cloudflare 守护程序,与 Cloudflare 云端通信,将云端请求数据转发到本地网络的 IP + 端口。

前置条件

  • 持有一个域名

  • 将域名 DNS 解析托管到 CloudFlare (我目前都直接转到了 CloudFlare )

  • 有一台服务器(本地非本地都可以,有没有公网 IP 都 OK),用于运行本地与 cloudflare 通信的 cloudflared 程序

  • 一张境内双币信用卡(仅用于添加付款方式,服务是免费的)

开始

1. 打开 Cloudflare Zero Trust 工作台面板

2. 创建 Cloudflare Zero Trust ,选择免费计划。需要提供付款方式,使用境内的双币卡即可

image.png

填写 team name,随意填写

image.png

选择免费计划

image.png

添加付款方式

image.png

填写信用卡信息(仅验证,不会扣款),完成配置

3. 完成后,在 Access Tunnels 中,创建一个 Tunnel。

image.png

创建 Tunnel

image.png

4. 选择 Cloudflared 部署方式。

Tunnel 需要通过 Cloudflared 来建立云端与本地网络的通道,这里推荐选择 Docker 部署 Cloudflared 守护进程以使用 Tunnel 功能。

image.png

获取 Cloudflared 启动命令及 Token

在本地网络主机上运行命令。我们还可以加上--name cloudflared -d --restart unless-stop为 Docker 容器增加名称和后台运行。你可以使用下方我修改好命令来创建 Docker,注意替换你为自己的 Token(就是网页中—-token 之后的长串字符)


docker run --name cloudflared -d --restart unless-stop cloudflare/cloudflared:latest tunnel --no-autoupdate run --token <YourToken>

5. 配置域名和转发 URL

为你的域名配置一个子域名(Subdomain),Path 留空,URL 处填写内网服务的 IP 加端口号。注意 Type 处建议使用 HTTP,因为 Cloudflare 会自动为你提供 HTTPS,因此此处的转发目标可以是 HTTP 服务端口。

image.png

配置内网目标 IP+端口

这里要注意,配置的 ip 如果是 127.0.0.1 或者是 localhost,是不行的

对于 linux 可以创建一个桥接网络

下面的 localNet 是网络名字,可自行修改;关于 192.168.0.0 这个子网,也可以自行定义.

默认按照下面的命令,执行后将可以通过 192.168.1.100 访问宿主机.


# 使用192.168.1.100替换127.0.0.1,如mongodb://192.168.1.100:27017

docker network create -d bridge --subnet 192.168.0.0/24 --gateway 192.168.1.100 localNet

完成

接着访问刚刚配置的三级域名,例如 https://app.yourdomain.com(是的,你没看错,是 https,cloudflare 已经自动为域名提供了 https 证书)就可以访问到内网的非公端口号服务了。一个 Tunnel 中可以添加多条三级域名来跳转到不同的内网服务,在 Tunnel 页面的 Public Hostname 中新增即可。

为你的服务添加额外验证

如果你觉得这种直接暴露内网服务的方式有较高的安全风险,我们还可以使用 Application 功能为服务添加额外的安全验证。

1. 点击 Application - Get started。

image.png

创建 Application

2. 选择 Self-hosted。

image.png

选择类型

3. 填写配置,注意 Subdomain 和 Domain 需要使用刚刚创建的 Tunnel 服务相同的 Domain 配置

image.png

配置三级域名

4. 选择验证方式。填写 Policy name(任意)。在 Include 区域选择验证方式,示例图片中使用的是 Email 域名的方式,用户在访问该网络时需要使用指定的邮箱域名(如@gmail.com)验证,这种方式比较适合自定义域名的企业邮箱用户。另外你还可以指定特定完整邮箱地址、IP 地址范围等方式。

image.png

选择验证方式

5. 完成添加

image.png

此时,访问 https://app.yourdomain.com 可以看到网站多了一个验证页面,使用刚刚设置的域名邮箱,接收验证码来访问。

image.png

评价

除了上述直接转发 http 服务之外,Tunnel 还支持 RDP、SSH 等协议的转发,玩法丰富。

作为一款免费的服务,简单的配置,低门槛使用条件,适合简单部署尝试。

通过 ssh 隧道部署,还可以跳过国内服务器域名必须备案的条件。

不过要注意的是 Tunnel 在国内访问速度不快,并且有断流的情况,请酌情使用。

]]>
Tunnel 可以做什么? 将本地网络的服务暴露到公网,可以理解为内网穿透。 将非常规端口服务转发到 80/443 常规端口。 自动为你的域名提供 HTTPS 认证。 为你的服务提供额外保护认证。 跳过国内服务器备案域名。 最重要的是——免费。
2023 年购物回忆录:值不值得买 2023-shopping-journey-value-and-regrets 2024-01-14T03:24:59+08:00 2024-01-14T03:24:59+08:00

2023年,对我来说是充满购物冒险的一年,我购买了很多东西,今天,我想回顾一下,分享哪些购买对我来说物超所值,哪些则是我冲动消费的产物,让我感到后悔。让我们开始这段回忆旅程吧~??️

超值购物体验

3D 打印机 拓竹 A1 2199RMB ?????

image.png

长久以来,我一直对3D打印充满好奇。23年年尾,拓竹推出了尺寸价格各方面都还蛮契合我期望的A1,首发抢到!也可能是因为到目前时间很短,新鲜感未消,所以,他是我综合来看,2023年购买的最值、体验最好的产品,当然我还有很多的坑的知识要去补,慢慢把它玩好,说不定以后再升级更贵的P系列呢哈哈~

spotify 会员 120RMB/年 爱合租 ???

使用的是爱合租这个平台(非广),已经忘了大概什么时候发现了爱合租这个平台,应该是之前推上有人说过还是谁的博客提了一嘴。

很多人说Spotify的推荐算法是最NB的,想从apple music 切过去看看,尝试尝试。所以我很早就下载体验了一下,但是单买太贵,而现在有了合租的渠道。

但是比较难受的是,苹果不支持语音控制播放,生态各方面不太行,快捷指令我也没有遇到什么合适的。总体来说,听歌的频率更低了,是不是也一方面说明我更忙了,没时间emo了?

youtube 会员 60RMB/年 ???

之前一直是 YouTube的免费用户,努力贡献广告收入,但是非会员无法后台播放,就很气,得一直开着前台,受不了简直是受不了了,从推上发现了一个卖的,他在tg上有频道专门卖,60一年,很便宜,确实解决了我的问题。

switch 双人成行 170RMB ?????

image.png

这应该是我在switch上通关的第一个大作,和女朋友一起玩的,确实很不错,是我玩过最好的双人游戏,也一定是目前switch上最好的双人游戏,值得每一对情侣去尝试。我比较晕3d(无缘塞尔达),但这个是可以玩的,游戏做的确实好,超值!

卤素台灯 165RMB ?????

image.png

由于家里没啥光照,窗户为了保暖我也给封起来了,23年尾,我想给家里养几盆花,由于之前我养的无一例外的挂掉了,这次我直接就想到了我需要准备好的光照,充足的光照。我就从网上找什么生物灯、补光灯,最后发现这款卤素台灯不错,外观也好看,既可以当台灯,也可以给植物补光,一整个契合我的需求,完美! 实际效果也非常非常给力,绿箩 发财树 常春藤都在夸夸长~

我后来买了一个小米智能插座3,配合快捷指令,每天语音、定时开关,体验翻倍!这里夸一下小米插座,其实小米插座也是一个五星产品哦~

后悔的购物决定

风行折叠车 5300RMB+无数配件 ?

image.png

今年换工作的时候上头买了一辆折叠车,花了我今年最贵的单品价格,我之前还专门写了一篇去记录整个购买过程,唉,大几千买了回来吃灰,哈哈哈哈 最亏的一个,希望24年夏天可以骑起来!

个人感慨

寻找生活的乐趣

2023年,我在购物上的探索,不仅仅是物质的积累,更是我对生活质量追求的体现。每一次点击“购买”,背后都有我对生活品质的向往和对新鲜事物的好奇。尤其是3D打印机拓竹A1,它不仅仅是一件商品,更是我梦寐以求的创造工具。每当我看到它打印出的第一个作品时,那种成就感和快乐是无法用言语表达的。

反思与成长

然而,并不是每一次的购物都带来了预期中的满足。比如风行折叠车,它让我意识到,有时候我们对事物的渴望,更多是一时的冲动,而非真正的需要。这种反思让我学会了在冲动购物和理智选择之间找到平衡。

未来展望

理性消费的决心

面对2024年,我计划更加理性地对待我的购物欲望。在购买前,我会花时间思考这件物品是否真正适合我的生活方式,它是否能长期为我的生活带来价值。同时,我也想尝试减少不必要的开支,专注于提升现有物品的使用体验,比如学习更多关于3D打印的技巧,或是开发更多与Spotify相结合的智能生活场景。

拥抱新技术,追求高质量生活

我对新技术和高质量生活的追求不会停止。在未来,我期待能遇到更多像卤素台灯这样的产品,它们不仅美观实用,还能提升我的生活品质。同时,我也希望能够更加注重生态与健康,例如选择更环保的产品,或是投资于提升居家环境的物品。

]]>
2023年,对我来说是充满购物冒险的一年,我购买了很多东西,今天,我想回顾一下,分享哪些购买对我来说物超所值,哪些则是我冲动消费的产物,让我感到后悔。让我们开始这段回忆旅程吧~??️
探索服务端通信技术:短轮询、WebSocket、SSE 与长轮询的深度比较 server-communication-methods 2023-11-19T13:51:46+08:00 2023-11-19T13:51:46+08:00

在现代 Web 应用中,服务端与客户端之间的高效通信至关重要。本文探讨了四种主流的服务端通信方法:短轮询、WebSocket、SSE(Server-Sent Events)和长轮询,分析它们的工作原理、适用场景及优缺点。

一、短轮询:高兼容性的传统选择

短轮询是服务端通信的一种基本方法,客户端通过定期发送 HTTP 请求来检查服务器上的更新。

image.png

  • 实际应用案例: 适用于新闻网站或博客的评论更新,用户可以在较短的时间内看到新的评论。

  • 优点: 高兼容性,适用于所有支持 HTTP 的客户端。

  • 缺点: 高资源消耗,频繁建立和关闭 TCP 连接。

  • 使用场景: 最适合不频繁更新且对实时性要求不高的应用。

最普通的一个场景:客户端定期向服务器请求最新消息


package main



import (

    "fmt"

    "net/http"

    "time"

)



func main() {

    http.HandleFunc("/poll", func(w http.ResponseWriter, r *http.Request) {

        // 假设这里是从数据库或某个存储检索最新消息的逻辑

        message := "Hello, world! - " + time.Now().Format(time.RFC1123)

        fmt.Fprint(w, message)

    })



    http.ListenAndServe(":8080", nil)

}

二、WebSocket:实时双向通信的理想选择

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,适用于需要实时双向通信的应用。

image.png

  • 实际应用案例: 在线游戏、股票交易平台,这些应用需要实时的数据交互。

  • 优点: 低延迟,适用于实时通信。

  • 缺点: 在某些网络环境下可能受限。

  • 使用场景: 高效的实时通信,特别是需要频繁数据交换的场景。

最常用的一个场景:实时聊天应用

服务端和客户端通过 WebSocket 保持连接,实现实时通讯。


package main



import (

    "net/http"

    "github.com/gorilla/websocket"

)



var upgrader = websocket.Upgrader{

    ReadBufferSize:  1024,

    WriteBufferSize: 1024,

}



func main() {

    http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {

        conn, _ := upgrader.Upgrade(w, r, nil) // 错误处理省略

        for {

            messageType, p, _ := conn.ReadMessage() // 错误处理省略

            conn.WriteMessage(messageType, p)       // 错误处理省略

        }

    })



    http.ListenAndServe(":8080", nil)

}



三、 SSE:简单的单向服务器推送技术

SSE 允许服务器向客户端单向发送更新,是一种基于 HTTP 的技术。

image.png

  • 实际应用案例: 实时新闻更新、股票市场的价格更新。

  • 优点: 实现简单,支持自动重连。

  • 缺点: 浏览器兼容性问题,无法实现双向通信。

  • 使用场景: 适用于单向数据流的应用,如实时新闻或股价更新。

一个典型的应用场景:股票价格更新


package main



import (

    "fmt"

    "net/http"

    "time"

)



func main() {

    http.HandleFunc("/sse", func(w http.ResponseWriter, r *http.Request) {

        w.Header().Set("Content-Type", "text/event-stream")

        w.Header().Set("Cache-Control", "no-cache")

        w.Header().Set("Connection", "keep-alive")



        for i := 0; ; i++ {

            fmt.Fprintf(w, "data: Price update %d\n\n", i)

            w.(http.Flusher).Flush()

            time.Sleep(time.Second * 2) // 模拟数据更新

        }

    })



    http.ListenAndServe(":8080", nil)

}



四、长轮询:短轮询的高效替代

长轮询在服务器有数据更新时才响应客户端请求,是短轮询的改进版。

  • 实际应用案例: 社交媒体通知更新,如 Facebook 或 Twitter 的新消息提示。

  • 优点: 减少不必要的轮询请求,相对于短轮询更高效。

  • 缺点: 仍需频繁建立和关闭连接。

  • 使用场景: 适用于对实时性要求不是非常高,但更新频率较高的场景。

我最近使用到的一个应用场景:AI 生图以后,通知给客户端


package main



import (

    "fmt"

    "net/http"

    "time"

)



func main() {

    messages := make(chan string) // 消息通道



    go func() {

        for {

            time.Sleep(time.Second * 5) // 每5秒生成一条消息

            messages <- fmt.Sprintf("New message at %v", time.Now())

        }

    }()



    http.HandleFunc("/long-poll", func(w http.ResponseWriter, r *http.Request) {

        message := <-messages // 等待新消息

        fmt.Fprint(w, message)

    })



    http.ListenAndServe(":8080", nil)

}



结论

在选择服务端通信技术时,应考虑应用的具体需求和场景。WebSocket 适合需要高实时性和双向通信的应用;SSE 适用于简单的单向数据推送;而短轮询和长轮询则适用于更新频率不高的场景。

选择合适的技术可以显著提高用户体验和应用性能。

我也曾遇到 WebSocket 一把梭的,在一些性能不敏感的地方,确实没有什么区别,但是,这不应该是一个技术人员的追求。

]]>
在现代 Web 应用中,服务端与客户端之间的高效通信至关重要。本文探讨了四种主流的服务端通信方法:短轮询、WebSocket、SSE(Server-Sent Events)和长轮询,分析它们的工作原理、适用场景及优缺点。
API 设计中的多类型属性选择:OpenAPI 与 gRPC 的 oneof 与强类型对比 api-design-openapi&grpc 2023-11-03T10:30:52+08:00 2023-11-03T10:30:52+08:00

在谈论 API 设计和开发时,有时,一个属性可以是多种类型中的一个,但不能同时是多种类型。比如支付接口的回调处理,常常为了兼容不同平台的参数,会使用以下方式中的一种来进行接收:

  1. 范型

  2. key-value 形式的 map

  3. 所有的 Object 都去接收,枚举哪个取哪个

但这种模式,往往会造成参数内容的不规范 、接口维护困难 或者是浪费网络传输带宽。

在程序开发中,我们往往会采用主流的 HTTP 协议和 gRPC 协议进行通信,两种技术都为开发者提供了强大的工具来描述、验证和生成 API,但它们的方法和原则有所不同。

OpenAPI 和 oneof

OpenAPI,早前被称为 Swagger,是一个用于描述 RESTful API 的规范。在其 3.0.1 版本中,引入了oneof关键字。

原因:

  • RESTful API 设计经常遇到一个属性可以是多种类型中的一个的情况。oneof提供了一种简单、明确的方式来描述这种复杂性。

好处:

  • 它使得模式更具表现力和灵活性,允许属性值匹配其中一个定义的模式。

gRPC 和 oneof

gRPC 使用 Protocol Buffers (ProtoBuf) 作为其接口定义语言。ProtoBuf 中也有一个oneof关键字,但其用途与 OpenAPI 中的略有不同。

原因:

  • 在 RPC 通信中,特别是在跨语言的场景下,有时需要表示一个值可以是多种数据类型中的一个。oneof为此提供了一个优雅的解决方案。

好处:

  • 它保证了在任何给定时间,oneof内的字段只能设置一个,这有助于节省存储空间和序列化/反序列化时间。

强类型支持

强类型是 OpenAPI 和 gRPC 都强烈支持的一个核心概念。

  • 强类型保证了数据的一致性:开发者在设计时定义了期望的数据类型,这有助于防止意外的类型错误。

  • 提高性能:知道数据的确切类型可以优化存储和访问。

我们可以达到的是

  • 错误检测:可以在编译时(或验证时,对于 OpenAPI)捕获类型错误,而不是在运行时。

  • 代码清晰:类型声明或注释为其他开发者提供了有关数据的清晰、明确的信息。

尽管 OpenAPI 和 gRPC 在处理oneof和强类型时有所不同,但它们的目标是相同的:提供明确、一致和可靠的 API 描述。选择哪种技术取决于具体的应用场景,但了解这些技术如何处理这些关键概念可以帮助开发者做出明智的决策。

]]>
在谈论 API 设计和开发时,有时,一个属性可以是多种类型中的一个,但不能同时是多种类型。比如支付接口的回调处理,常常为了兼容不同平台的参数,会使用以下方式中的一种来进行接收,在程序开发中,我们往往会采用主流的 HTTP 协议和 gRPC 协议进行通信,两种技术都为开发者提供了强大的工具来描述、验证和生成 API,但它们的方法和原则有所不同。
团队开发中对于代码仓库和镜像管理的标准统一 unified-standards-code-management-in-team-development 2023-10-25T09:01:39+08:00 2023-10-25T09:01:39+08:00

随着现代软件开发的持续迭代,团队间的协作变得越来越重要。为了确保整个团队在编码、构建和部署过程中的效率和质量,引入一些特定的配置文件和标准工作流程显得尤为关键。以下就是一些在团队开发中常见而又重要的文件,我总结了它们的一些作用和优点。

1. .editorconfig

作用.editorconfig 是一种定义代码格式约定的文件,它用于统一不同编辑器和 IDE 的代码格式设置。

好处

  • 保证团队成员间代码风格的一致性。

  • 避免因为不同的代码风格导致的无意义的代码差异。

  • 提高代码的可读性和维护性。

2. .dockerignore

作用:定义哪些文件或目录应该被 Docker 忽略,不被包括在镜像构建中。

好处

  • 减少 Docker 镜像的大小。

  • 加快构建速度。

  • 避免将敏感或不必要的文件包含在 Docker 镜像中。

3. .gitignore

作用:指定哪些文件或目录应该被 Git 忽略,不被包括在版本控制中。

好处

  • 保持仓库的整洁。

  • 避免将编译产物、日志、临时文件等不必要的文件添加到 Git 中。

  • 防止敏感数据泄露。

4. .http 文件

作用:这是一个用于 API 测试的文件,可以在一些 IDE 如 Visual Studio Code、IDEA 中直接运行。

好处

  • 简化 API 测试流程。

  • 提供一个可共享和可版本化的 API 测试集合。

5. Dockerfile

作用:描述如何自动构建 Docker 镜像的指南。

好处

  • 保证镜像构建的一致性。

  • 简化和标准化部署流程。

  • 提供一个清晰的、版本化的构建描述。

6. Makefile

作用:定义一组任务,如编译、测试和部署等,以自动化工作流程。

好处

  • 提高工作效率,简化复杂任务。

  • 为新成员提供了一个明确的指南。

  • 保证任务的一致性和可重复性。

7. Jenkinsfile

作用:描述如何在 Jenkins 上自动化构建、测试和部署流程。

好处

  • 实现持续集成和持续部署。

  • 提高代码质量,通过自动化测试减少错误。

  • 加速软件交付速度。

以上所列出的各种文件在整个 CI/CD 中都很重要。它们不仅可以帮助团队保持代码的一致性、高质量和安全性,还可以大大提高开发和部署的效率。如果你的团队中有哪个工具没有使用到,很开心可以分享给你,如果你觉得有什么同样重要的文件我没有提到,欢迎留言。

]]>
随着现代软件开发的持续迭代,团队间的协作变得越来越重要。为了确保整个团队在编码、构建和部署过程中的效率和质量,引入一些特定的配置文件和标准工作流程显得尤为关键。以下就是一些在团队开发中常见而又重要的文件,我总结了它们的一些作用和优点。
我的自行车 fnhon 2023-10-20T10:40:33+08:00 2023-10-20T10:40:33+08:00

在今年7月份左右,我开始关注自行车,出于便携性而言,我更倾向于购买一辆折叠车,之前在大街上也看到了很多骑得超快的折叠车,所以我对于折叠车的竞速性能也是满意的,于是我开始看各种折叠车的测评和论坛。

折叠车和山地、公里车相比,会更加复杂,零件会更多一些。同样的,有整车 有组装。但在折叠车的市场里,不想公路和山地,大多数购买整车,在折叠车这里,组装是主市场。

组装,主要的零件就是车架,折叠车里最火的就是风行的车架。除了风行,另外就是大行、飞鱼 和 小布,大行是卖的最多的折叠车整车品牌,小布是一个很“高端豪华”设计很时尚的很贵的折叠车品牌。

风行蚂蚁腿:

image.png

大行P8:

image.png

小布折叠车:

image.png

因为风行折叠车的可选配、自定义方案足够多,所以我选择了从风行中寻找方案。风行的折叠车主要有四种车架,蚂蚁腿、Y架、K架、海豚架。


而我的出行并没有特别的需求,通勤短程使用,没有特别的需求,好看即可,而蚂蚁腿就是颜值这块儿的顶梁柱。

然后我看了很多的方案,看到最后,给我审美整疲劳了,最后把之前觉得好看的发给了女朋友,让她来给我选,最后,她一眼就看中了非碳的最贵的一个。。。

image.png

image.png

image.png

image.png

这个车架的颜色非常漂亮,有风行的标,但不是风行的颜色。问过老板才知道,是他们家自己喷的,我再翻,就没有过第二家这么做的了。

整车配置如下:

56101696910294_.pic.jpg

车架大概是 1200 + 700 涂装

两个轱辘 加一起 1k多

碳前叉 1k

加上其他的零零散散碎碎 ¥5300

看到这个价格,我当时确实是跪了。

买车最开始嚷嚷着预算800, 后来看着看着 1600, 再看风行折叠车9速最低配也要2500,好家伙一层一层,最后终于超越了5k。。。

可能是价格的烘托,这车是越看越好看。

女朋友觉得我喜欢的东西,买就完了。想到自己今年要换工作,工资也涨了一点点,买!

随后我也是痛痛快快下单了。

经过半个多月的漫长等待,商家终于想起我了,然后用了一天给我喷了漆,连夜顺丰,给我发过来了。


经过比较漫长的挑选和等待,我终于收到了心爱的车~

开箱,店家送了一副鲜红色的脚踏板,我透,然后我和店家说颜色不合适呀,店家又给我发了一个。

image.png

就是上面两幅,黑的发来的是个坏的,左脚螺丝口对的有问题,装上骑也有异响。我忍不了等待了,自己花一百多买了一副。 然后就是把套,也是在上图,是一个海绵把套,骑了几公里后我发现手腕疼,赶紧换! 海绵把套还贼难拆,直接刀子剌掉。

考虑过上弯把和牛角把,像这样的:

image.png

但是发现,会影响折叠(特别是我的车头管是内折的)。所以最后选了一个大行的把套,感觉还是不错的。

image.png

关于折叠车,业界是标配不上脚撑子和挡泥板的,我装一下,我也不上!

为了安全,买了车前灯和尾灯还有铃铛,买了自行车挎包 买了骑行护目镜,头盔目前还没有买。


至此,我的自行车相关配件也购买完毕。

开始遇到一些问题:

  1. 骑行异响

骑的时候,不是牙盘响,认真听 是车前面的部分。后来联系了商家,确认不会是别的问题,我就往前把的各种关节加了WD-40,解决!

  1. 换挡不流畅

5挡以下,换挡来回跳,后来问了下店家,说后面有一个档位的微调螺丝,调整下就好了,我就在车库调调骑骑,弄得差不多算流畅吧。

另外还有一个温馨提示,我这个车是碟刹嘛,刹车动作如果比较多,骑完车一定不要手贱去摸那个刹车片。


可以开始畅快地骑行了,然后,我发现,公司不让把自行车折叠带上楼!!! 放楼下又怕刮蹭,毕竟这车架就小两千啦,还是心疼的。

再往后两天,我又换了工作,离家17公里多,啊~ 骑不动了,不骑了

吃灰开始。

]]>
在今年7月份左右,我开始关注自行车,出于便携性而言,我更倾向于购买一辆折叠车,之前在大街上也看到了很多骑得超快的折叠车,所以我对于折叠车的竞速性能也是满意的,于是我开始看各种折叠车的测评和论坛~
使用 rclone 命令行管理 Cloudflare R2 对象存储 r2-rclone 2023-10-13T07:00:26+08:00 2023-10-13T07:00:26+08:00

Cloudflare R2 是一款高性能、低成本的对象存储服务。之前在使用 OSS 的时候,用惯了 OSS brewer, 在用 R2 时候发现,开始没有找到合适的 brewer 工具,而网页端上传会限制文件数量和大小,就尝试用命令行的 client 了哈哈, 使用之后发现,确实好用,相比使用网页管理对象存储,利用命令行工具可以大大提高管理效率,特别适合需要批量操作或脚本化的场景。

评论老哥分享了一个 client 工具: https://cyberduck.io/ 我尝试用下试试~

安装 rclone


sudo -v ; curl https://rclone.org/install.sh | sudo bash

查看 config 文件识别的地址


rclone config file

# Configuration file doesn't exist, but rclone will use this path:

# /Users/xxx/.config/rclone/rclone.conf


# 编辑文件配置

vi /Users/xxx/.config/rclone/rclone.conf


[testConfig]

type = s3

provider = Cloudflare

access_key_id = abc123

secret_access_key = xyz456

endpoint = https://<accountid>.r2.cloudflarestorage.com

acl = private

这里我创建了一个名为 bucket 的桶

所需的 access_key_id 和 secret_access_key 需要另外申请:

image.png

设置完成后,就可以对 cloudflare 的桶资源进行管理了。

列出存储桶和对象


# 列出所有存储桶:

rclone lsd testConfig:



# 列出所有桶及内部目录文件

~ rclone tree testConfig:

  ...



# 列出指定桶及内部目录文件

~ rclone tree testConfig:bucket

/

└── 2023

    ├── 08

    │   ├── 1690960360295.png

    │   └── 1692713312723.png

    ├── 09

    │   ├── 1693822893.jpg

    │   └── 1694588667.png

    └── 10

        └── 1696911351.png

        

# 创建新桶: 

rclone mkdir testConfig:bucket



# 删除空桶:

rclone rmdir testConfig:bucket  



# 列出对象列表:

rclone ls testConfig:path

# 计算对象存储总量:

rclone size testConfig:path

上传和检索对象


# 上传本地文件或目录: 

# rclone copy [目录或者文件] test:桶名+路径

rclone copy helloworld testConfig:bucket



# 查看

rclone tree testConfig:bucket



# 下载对象到本地:

# rclone copy test:桶名+路径 本地目标路径

rclone copy testConfig:bucket/README.md .



# 删除

# rclone delete test:桶名+路径

rclone delete testConfig:bucket/README.md



# 更多

rclone --help



]]>
Cloudflare R2 是一款高性能、低成本的对象存储服务。之前在使用 OSS 的时候,用惯了 OSS brewer, 在用 R2 时候发现,开始没有找到合适的 brewer 工具,而网页端上传会限制文件数量和大小,就尝试用命令行的 client 了哈哈, 使用之后发现,确实好用,相比使用网页管理对象存储,利用命令行工具可以大大提高管理效率,特别适合需要批量操作或脚本化的场景。
GORM 中 SQL、慢 SQL 打印日志传递 trace ID gorm-trace-id 2023-09-25T11:14:27+08:00 2023-09-25T11:14:27+08:00

实现 gorm.io/gorm/logger 下的函数⬇️


// gorm 源码

type Interface interface {  

   LogMode(LogLevel) Interface  

   Info(context.Context, string, ...interface{})  

   Warn(context.Context, string, ...interface{})  

   Error(context.Context, string, ...interface{})  

   Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error)  

}

以下为自定义的重写实现,主要函数是Trace


package log  

  

import (  

   "context"  

   "time"  

   "github.com/go-kratos/kratos/v2/log"

   "gorm.io/gorm/logger"

)  

  

type GormLogger struct {  

   SlowThreshold time.Duration  

}  

  

func NewGormLogger() *GormLogger {  

   return &GormLogger{  

      SlowThreshold: 200 * time.Millisecond, // 一般超过200毫秒就算慢查所以不使用配置进行更改  

   }  

}  

  

var _ logger.Interface = (*GormLogger)(nil)  

  

func (l *GormLogger) LogMode(lev logger.LogLevel) logger.Interface {  

   return &GormLogger{}  

}  

func (l *GormLogger) Info(ctx context.Context, msg string, data ...interface{}) {  

   log.Context(ctx).Infof(msg, data)  

}  

func (l *GormLogger) Warn(ctx context.Context, msg string, data ...interface{}) {  

   log.Context(ctx).Errorf(msg, data)  

}  

func (l *GormLogger) Error(ctx context.Context, msg string, data ...interface{}) {  

   log.Context(ctx).Errorf(msg, data)  

}  

func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {  

   // 获取运行时间  

   elapsed := time.Since(begin)  

   // 获取 SQL 语句和返回条数  

   sql, rows := fc()  

   // Gorm 错误时打印

   if err != nil {  

      log.Context(ctx).Errorf("SQL ERROR, | sql=%v, rows=%v, elapsed=%v", sql, rows, elapsed)  

   }  

   // 慢查询日志  

   if l.SlowThreshold != 0 && elapsed > l.SlowThreshold {  

      log.Context(ctx).Warn("Database Slow Log, | sql=%v, rows=%v, elapsed=%v", sql, rows, elapsed)  

   }  

  

}

这里我使用的log组件是kratos框架的log组件,设置好zap后注入为全局log

GORM创建连接的地方注入我们重写后的自定义Logger


db, err := gorm.Open(mysql.Open(cfg.Dsn()), &gorm.Config{  

   QueryFields: true,    

   Logger: logdef.NewGormLogger(),  // 注入

})

最后,在查询的地方,带上withContext即可


func (ud *userDao) AddOne(c context.Context, user *model.User) (userId int64, err error) {  

   err = db.GetConn().WithContext(c).Create(user).Error  

   if err != nil {  

      log.Context(c).Errorf("AddOne fail|err=%+v", err)  

      return  

   }  

   return user.Id, nil  

}

]]>
GORM 中SQL、慢SQL打印日志传递 trace ID, 重写GORM日志实现,SQL日志、慢日志请求追踪
更换博客图床,从 OSS 到 GitHub 再到 R2 pic-archive 2023-08-02T09:09:40+08:00 2023-08-02T09:09:40+08:00

写博客一个很重要的东西就是图床, 如果没有一个合适的图床来安全稳定地存储数据,指不定哪天,你的文章就到处都是

真的找不到了.png

而对于一个曾经被ban过号,崩过盘(硬盘)的人来说,分外敏感。如果你的图片存储在任何一个带监管的平台,你的账号很可能由于一些不可抗力资源牵连,内容全部404。

于是我对自己的博客相关的资源都进行了“安全”的部署,图片资源这块儿一年多来,我一直使用的是github做图床的,但是github有几个问题:

  • 一个仓库的存储大小限制1G,如果超过限制,很可能会被ban库的。

  • GitHub 阻止大小超过 100 MB 的文件。

  • GitHub 在国内网络下无法访问。

于是,几个月前把云资源全部迁到cloudflare的我,又有了白嫖的念头。

能不能使用R2来做图床呢?之前用过阿里OSS做图床的我,本能地想到,一定是可以的!

简单搜搜,R2图床~

不出所料~哈哈哈哈简简单单

当然前提是你需要有一个cloud flare 账号哦

一. cloud flare操作

1. 到cloud flare创建一个bucket

image.png

2. 对bucket进行配置

image.png

3. 然后到r2的列表菜单,申请一个secret

image.png

image.png

二. 本地操作

使用pic-go进行上传,如果没有,请安装一个

1. 在设置中的默认图床不包含cloud flare ,需要安装一下插件,这里的插件需要安装的是aws的S3插件,该插件兼容国外同S3类型的这些所有的对象存储。

需要安装这两个插件,一个是功能,一个是展示。

image.png

然后在设置处就会出来新的选项

image.png

2. 填写下刚才获取到的那些参数

image.png

最后效果如下:


![image.png](https://pic.whrss.com/2023/08/1690961579898.png)

完美~

]]>
写博客一个很重要的东西就是图床, 如果没有一个合适的图床来安全稳定地存储数据,指不定哪天,你的文章就到处都是 ![真的找不到了.png](https://pic.whrss.com/1690960055739.png) 而对于一个曾经被ban过号,崩过盘(硬盘)的人来说,分外敏感。如果你的图片存储在任何一个带监管的平台,你的账号很可能由于一些不可抗力资源牵连,内容全部404。 于是我对自己的博客相关的资源都进行了“安全”的部署,图片资源这块儿一年多来,我一直使用的是github做图床的,但是github有几个问题...
如何在 Go 中实现程序的优雅退出,go-kratos 源码解析 go-grace-stop 2023-07-29T05:58:45+08:00 2023-07-29T05:58:45+08:00

使用kratos这个框架有近一年了,最近了解了一下kratos关于程序优雅退出的具体实现。

这部分逻辑在app.go文件中,在main中,找到app.Run方法,点进入就可以了

它包含以下几个部分:

  1. App结构体:包含应用程序的配置选项和运行时状态。

  2. New函数:创建一个App实例。

  3. Run方法:启动应用程序。主要步骤包括:

  • 构建ServiceInstance注册实例

  • 启动Server

  • 注册实例到服务发现

  • 监听停止信号

  1. Stop方法:优雅停止应用程序。主要步骤包括:
  • 从服务发现中注销实例

  • 取消应用程序上下文

  • 停止Server

  1. buildInstance方法:构建用于服务发现注册的实例。

  2. NewContext和FromContext函数:给Context添加AppInfo,便于后续从Context获取。

核心的逻辑流程是:

  1. 创建App实例

  2. 在App.Run()里面启动Server,注册实例,监听信号

  3. 接收到停止信号后会调用App.Stop()停止应用

我们先对Run方法进行一个源码进行查看


// Run executes all OnStart hooks registered with the application's Lifecycle.

func (a *App) Run() error {



  // 构建服务发现注册实例

  instance, err := a.buildInstance() 

  if err != nil {

    return err

  }



  // 保存实例  

  a.mu.Lock()

  a.instance = instance

  a.mu.Unlock()



  // 创建错误组

  eg, ctx := errgroup.WithContext(NewContext(a.ctx, a))



  // 等待组,用于等待Server启动完成

  wg := sync.WaitGroup{}



  // 启动每个Server

  for _, srv := range a.opts.servers {

    srv := srv 

    eg.Go(func() error {

      // 等待停止信号

      <-ctx.Done()  

      // 停止Server

      stopCtx, cancel := context.WithTimeout(a.opts.ctx, a.opts.stopTimeout)

      defer cancel()

      return srv.Stop(stopCtx)

    })



    wg.Add(1)

    eg.Go(func() error {

      // Server启动完成

      wg.Done() 

      // 启动Server  

      return srv.Start(NewContext(a.opts.ctx, a)) 

    })

  }



  // 等待所有Server启动完成

  wg.Wait()



  // 注册服务实例

  if a.opts.registrar != nil {

    rctx, rcancel := context.WithTimeout(ctx, a.opts.registrarTimeout)

    defer rcancel()

    if err := a.opts.registrar.Register(rctx, instance); err != nil {

      return err

    }

  }

  

  // 监听停止信号

  c := make(chan os.Signal, 1)

  signal.Notify(c, a.opts.sigs...)

  eg.Go(func() error {

    select {

    case <-ctx.Done():

      return nil

    case <-c:

      // 收到停止信号,停止应用------------- ⬅️注意此时

      return a.Stop() 

    }

  })



  // 等待错误组执行完成

  if err := eg.Wait(); err != nil && !errors.Is(err, context.Canceled) {

    return err

  }



  return nil

}

核心逻辑就是这里⬇️,使用signal.Notify去监听操作系统给出的停止信号。


  // 监听停止信号

  c := make(chan os.Signal, 1)

  signal.Notify(c, a.opts.sigs...)

  eg.Go(func() error {

    select {

    case <-ctx.Done():

      return nil

    case <-c:

      // 收到停止信号,停止应用

      return a.Stop() 

    }

  })

然后调用了Stop方法,我们再看下Stop的源码


// Stop gracefully stops the application.

func (a *App) Stop() error {



  // 获取服务实例 

  a.mu.Lock()

  instance := a.instance

  a.mu.Unlock()



  // 从服务发现注销实例

  if a.opts.registrar != nil && instance != nil {

    ctx, cancel := context.WithTimeout(NewContext(a.ctx, a), a.opts.registrarTimeout)

    defer cancel()

    if err := a.opts.registrar.Deregister(ctx, instance); err != nil {

      return err

    }

  }



  // 取消应用上下文

  if a.cancel != nil {

    a.cancel() 

  }



  return nil

}

主要步骤是:

  1. 获取已经保存的服务实例

  2. 如果配置了服务发现,则从服务发现中注销该实例

  3. 取消应用上下文来通知应用停止

在Run方法中,我们通过context.WithCancel创建的可取消的上下文Context,在这里通过调用cancel函数来取消该上下文,以通知应用停止。

取消上下文会导致在Run方法中启动的协程全部退出,从而优雅停止应用。

所以Stop方法比较简单,关键是利用了Context来控制应用生命周期。

我们可以注意到,在Run方法中,我们使用到了一个signal包下的Notify方法来对操作系统的关闭事件进行监听,这个是我们动作的核心,我把这部分单独整理在了另一篇文章中。

通过对操作系统事件的监听,我们就可以对一些必须完成的任务进行优雅地停止,如果有一些任务必须完成,我们可以在任务开始使用 wg := sync.WaitGroup{} 来对任务进行一个Add操作,当所有任务完成,监听到操作系统的关闭动作,我们需要使用wg.wait() 等待任务完成再进行退出。以实现一个优雅地启停。

]]>
使用kratos这个框架有近一年了,最近了解了一下kratos关于程序优雅退出的具体实现。
os.signal golang 中的信号处理 go-signal 2023-07-29T05:56:55+08:00 2023-07-29T05:56:55+08:00

在程序进行重启等操作时,我们需要让程序完成一些重要的任务之后,优雅地退出,Golang为我们提供了signal包,实现信号处理机制,允许Go 程序与传入的信号进行交互。

Go语言标准库中signal包的核心功能主要包含以下几个方面:

1. signal处理的全局状态管理

通过handlers结构体跟踪每个signal的处理状态,包含信号与channel的映射关系,以及每个信号的引用计数。

2. 信号处理的注册与注销

Notify函数用于向指定的channel注册信号处理,会更新handlers的状态。

Stop函数用于注销指定channel的信号处理,将其从handlers中移除。

Reset函数用于重置指定信号的处理为默认行为。

3. 信号的抓取与分发

process函数在收到signal时,会把它分发给所有注册了该信号的channel。

4. signal处理的恢复

通过cancel函数,可以恢复signal的默认行为或忽略。

5. Context信号通知支持

NotifyContext函数会创建一个Context,在Context结束时自动注销signal处理。

6. 处理signal并发访问的同步

通过handlers的锁保证对全局状态的线程安全访问。

7. 一些工具函数

如handler的mask操作,判断signal是否在ignore列表中等。

总的来说,该实现通过handlers跟踪signal与channel的关系,在收到signal时分发给感兴趣的channel,提供了flexible和高效的signal处理机制。

在实际地使用中,我们需要创建一个接收信号量的channel,使用Notify将这个channel注册进去,当信号发生时,channel就可以接收到信号,后续的业务就可以针对性地进行处理。如下:


package main



import (

"fmt"

"os"

"os/signal"

"syscall"

)



func main() {



// 创建一个channel来接收SIGINT信号

c := make(chan os.Signal)



// 监听SIGINT信号并发送到c

signal.Notify(c, syscall.SIGINT)



// 使用一个无限循环来响应SIGINT信号

for {

fmt.Println("Waiting for SIGINT")

<-c

fmt.Println("Got SIGINT. Breaking...")

break

}

}

共有32个信号量,相对应的枚举在syscall包下

常用的信号值包括:

SIGHUP 1 终端控制进程结束(终端连接断开)

SIGINT 2 用户发送INTR字符(Ctrl+C)触发

SIGQUIT 3 用户发送OUIT字符(Ctrl+/触发

SIGKILL 9 无条件结束程序(不能被捕获、阻塞或忽略)

SIGUSR1 10 用户保留

SIGUSR2 12 用户保留

SIGPIPE 13 消息管道损坏(FIFO/Socket通信时,管道未打开而进行写操作)

SIGALRM 14 时钟定时信号

SIGTERM 15 结束程序(可以被捕获、阻塞或忽略)

在go框架中,项目中实际使用到signal进行优雅退出见:如何在go中实现程序的优雅退出,go-kratos源码解析

]]>
在程序进行重启等操作时,我们需要让程序完成一些重要的任务之后,优雅地退出,Golang为我们提供了signal包,实现信号处理机制,允许Go 程序与传入的信号进行交互。
探索 PlanetScale:划分分支的 MySQL Serverless 平台 planet-scale 2023-07-12T03:37:52+08:00 2023-07-12T03:37:52+08:00

最近我发现了一个非常有趣的国外MySQL Serverless平台,它叫做PlanetScale。这个平台不仅仅是一个数据库,它能像代码一样轻松地创建开发和测试环境。你可以从主库中拉出一个与之完全相同结构的development或staging数据库,并在这个环境中进行开发和测试。所有的数据都是隔离的,不会相互干扰。

当你完成开发后,你可以创建一个deploy request,PlanetScale会自动比对并生成Schema diff,然后你可以仔细审查需要部署的内容。确认没问题,你就可以将这些变更部署到线上库中。整个部署过程不会导致停机时间,非常方便。

PlanetScale的入门使用是免费的,他们提供了以下免费套餐:

  • 5GB存储空间

  • 每月10亿行读取操作

  • 每月1000万行写入操作

  • 1个生产分支

  • 1个开发分支

  • 社区支持

如果超出了免费套餐的限制,他们会按照以下价格收费:每GB存储空间每月2.5美元,每10亿行读取操作每月1美元,每100万行写入操作每月1.5美元。对于我这样的个人使用者,真的太不错了。

这个平台运行在云上,提供了一个Web管理界面和一个CLI工具。我试了一下他们的Web管理界面,但发现它并不是很好用,无法进行批量的SQL执行。于是我研究了一下CLI工具的使用,并做了一份小记录,现在和大家分享一下。

以下是在 Mac 中使用PlanetScale CLI工具的步骤:

其他系统安装可见:官方文档

1. 安装pscale工具

brew install planetscale/tap/pscale

2. 更新brew和pscale,确保使用的是最新版本

brew update && brew upgrade pscale

3. 进行认证

pscale auth login

这个命令会在浏览器中打开一个页面

image.png

如果你已经登录了PlanetScale账号,它会直接让你确认验证。验证成功后,你就可以开始使用CLI工具了。

如果你走到这步的时候提示:


Error: error decoding error response: invalid character '<' looking for beginning of value

你需要调整一下网络~ 目前是不给大陆用户IP使用的。

4. 连接到相应的数据库分支

pscale connect [数据库名] [分支名] # 例如: pscale connect blog main

连接成功后,你就可以通过本地的3306端口代理访问远程数据库了。


Secure connection to database whrss and branch main is established!.

Local address to connect your application: 127.0.0.1:3306 (press ctrl-c to quit)

image.png

5. 本地连接

点击Get connection strings,你就可以得到连接数据库所需的账号名和密码,然后可以在本地的数据库连接软件中直接连接数据库了。

![](https://raw.githubusercontent.com/whrsss/pic-sync/master/img/202307121100351.png)
  1. 选择适合你的编程语言的连接串,这样你就可以在不同的程序中直接使用了。

    image.png

通过这些简单的步骤,你就可以轻松地使用PlanetScale来管理和部署你的MySQL应用了。快来体验一下吧!

]]>
最近我发现了一个非常有趣的国外MySQL Serverless平台,它叫做PlanetScale。这个平台不仅仅是一个数据库,它能像代码一样轻松地创建开发和测试环境。你可以从主库中拉出一个与之完全相同结构的development或staging数据库,并在这个环境中进行开发和测试。所有的数据都是隔离的,不会相互干扰。 当你完成开发后,你可以创建一个deploy request,PlanetScale会自动比对并生成Schema diff,然后你可以仔细审查需要部署的内容。确认没问题,你就可以将这些变更部署到线上库中。整个部署过程不会导致停机时间,非常方便。
优化你的RSS订阅:一次全面改进的实践 feed-sub 2023-06-26T20:25:21+08:00 2023-06-26T20:25:21+08:00

先前,我的 RSS 订阅功能过于简化,只提供了几个基本字段,而且不展示全文。简介后面,我添加了一个链接指向原文,如下所示:

image.png

过于简陋,无法直接在 RSS 阅读器软件上进行查看,另外,我临时用 Kotlin 编写的这个功能需要每次调用时重新生成,这点就对这个功能的简单和独立性产生了影响,这件事放了太久,最近进行一次全面的改进。

原始的文章是用 Markdown 编写的。为了方便通过手机或电脑上的 RSS 阅读器查看,我需要将文章内容转换为 HTML 格式。在 Go 语言中,有一个成熟的组件:blackfriday。它能把每段文字用 p 标签标记,用 code 和 pre 标签标记代码块,以及用 h1234 标签标记不同层级的标题。


github.com/PuerkitoBio/goquery

github.com/russross/blackfriday/v2

经过一次生成并查看效果后,我发现有些地方需要改进。例如,图片和文字都是左对齐的,看起来并不美观。另外,非代码块的单引号也会被转化为代码块,导致原本可以在一行显示的内容被拆分为三行。于是我添加了一些附加功能:


func Md2Html(markdown []byte) string {



// 1. Convert markdown to HTML

html := blackfriday.Run(markdown)



// 2. Create a new document from the HTML string

doc, err := goquery.NewDocumentFromReader(bytes.NewReader(html))

if err != nil {

log.Fatal(err)

}



// 3. Process all elements to have a max-width of 1300px and text alignment to left

doc.Find("p, h1, h2, h3, h4, h5, h6, ul, ol, li, table, pre").Each(func(i int, s *goquery.Selection) {

s.SetAttr("style", "max-width: 1300px; display: block; margin-left: auto; margin-right: auto; text-align: left;")

})



// 4. Process the images to be centered and have a max size of 500x500

doc.Find("img").Each(func(i int, s *goquery.Selection) {

s.SetAttr("style", "max-width: 500px; max-height: 500px; display: block; margin-left: auto; margin-right: auto;")

})



// 5. Process code blocks to be styled like in markdown, and inline code to be bold

doc.Find("code").Each(func(i int, s *goquery.Selection) {

if goquery.NodeName(s.Parent()) == "pre" {

// this is a code block, keep the markdown style

s.SetAttr("style", "display: block; white-space: pre; border: 1px solid #ccc; padding: 6px 10px; color: #333; background-color: #f9f9f9; border-radius: 3px;")

} else {

// this is inline code, replace it with bold text

s.ReplaceWithHtml("<b>" + s.Text() + "</b>")

}

})



// 6. Get the modified HTML

modifiedHtml, err := doc.Html()

if err != nil {

log.Fatal(err)

}



// Replace self-closing tags

modifiedHtml = strings.Replace(modifiedHtml, "/>", ">", -1)



return modifiedHtml

}

修改之后,我把生成的 HTML 内容放入 RSS 的 XML 中。XML 内容的生成就是简单的字符串拼接,我写了一个 GenerateFeed 方法来完成:


type Article struct {

Title string

Link string

Id string

Published time.Time

Updated time.Time

Content string

Summary string

}



func GenerateFeed(articles []Article) string {

// 对文章按发布日期排序

sort.Slice(articles, func(i, j int) bool {

return articles[i].Published.After(articles[j].Published)

})



// 生成Feed

feed := `<feed xmlns="http://www.w3.org/2005/Atom">

<title>了迹奇有没</title>

<link href="/feed.xml" rel="self"/>

<link href="https://whrss.com/"/>

<updated>` + articles[0].Updated.Format(time.RFC3339Nano) + `</updated>

<id>https://whrss.com/</id>

<author>

<name>whrss</name>

</author>`



for _, article := range articles {

feed += `

<entry>

<title>` + article.Title + `</title>

<link href="` + article.Link + `"/>

<id>` + article.Id + `</id>

<published>` + article.Published.Format(time.RFC3339) + `</published>

<updated>` + article.Updated.Format(time.RFC3339Nano) + `</updated>

<content type="html"><![CDATA[` + article.Content + `]]></content>

<summary type="html">` + article.Summary + `</summary>

</entry>`

}



feed += "\n</feed>"

return feed

}

最后效果确实十分理想,手机上的观看效果不输我的原始站点,很不错~

]]>
先前,我的 RSS 订阅功能过于简化,只提供了几个基本字段,而且不展示全文。我决定对订阅功能做一次全面的改进。
最近在做的事:GitHub Action | GPT Plus | whisper | V2EX | GPT API | PMP done-lately 2023-06-25T15:49:29+08:00 2023-06-25T15:49:29+08:00

运动数据使用GitHub Action自动更新

看着每天的运动数据,满满的,还感觉有点充实:tw-1f606: 积少成多嘛

开通体验了一下GPT Plus

太想体验下GPT4了,之前苦于充值门槛过高,上个月出了app充值后,我使用支付宝礼品卡充值体验了下,感觉还是不错的,但是个人使用确实还是按量更划算一点,但是充值门槛过高:tw-1f632:

买了一支录音笔,全天候录音,使用 whisper 录音转文字

之前全程吃瓜了刘能离婚案,后来受他种草,也买了索尼的tx660,全天候录音,之后再把录音源文件和转的文本保存起来,这样想检索也会很方便的。

终于注册了 V2EX

这个社区知道很久了,一直是游客登录,看到别人发的别人社区啊推荐啊很不错,我说我也得养养号:tw-1f61c:

GPT API续费问题曲线救国

续上面充值问题,最近充值门槛更高了,对网络要求也更高了,还动不动就给封号。那我怎么整,Api确实便宜又方便,而且不用管那么复杂的网络。这不刚注册了V2EX嘛,上去翻翻。

image.png

然后我使用了这个-> aiproxy ,咱这V站注册就开始实际生产力了哈哈哈。

感觉还可以,然后使用自建的客户端输入地址和key就可以用啦。

image.png

报名了PMP的培训班和考试

6月中下旬决定报名8月份的PMP考试,一方面是我现在处于软考之后的一个学习的空窗期,一个原因是因为朋友刘某(一个年轻的项目经理)拉我,最重要的一个原因是,我确实很需要管理协调方面的工作技能,而且我刚好到了PMP的报名门槛:tw-1f604:。

报名费是真滴贵呀~

]]>
运动数据使用GitHub Action自动更新| 开通体验了一下GPT Plus | 买了一支录音笔,全天候录音,使用 whisper 录音转文字 |终于注册了 V2EX |GPT API续费问题曲线救国|报名了PMP的培训班和考试
一个游戏开发者的自我介绍 me 2023-06-08T22:22:30+08:00 2023-06-08T22:22:30+08:00

我写这篇博客的初衷并不是出于单纯的自我表达,而是为了那些可能会对我未来职业发展产生重要影响的人们。或许你是一个正在考虑我的简历的HR,或者是将来可能与我一起共事的团队成员。无论你是谁,我都希望通过这篇博客让你更深入地了解我。同时,写作的过程也让我有机会重新审视和整理我的技能和经历。:tw-1f61d:

我目前就职于一家游戏公司,我们的团队虽然但十分精悍:tw-270a:。我负责的任务非常多,目前核心是一个女性向AVG游戏的后端开发,这款游戏在TapTap上的评价和排名都 ,另外我也参与公共服务和核心业务模块的开发。

除此之外,我还负责管理公司所有的服务器资源,并负责运维工作。这主要得益于我平时对很多技术的尝试和钻研,让我能够顺利应对上云带来的各种挑战。

我在2021年3月加入这家公司,两个月后便开始接手核心项目。主要的技术栈包括Kratos + GoSpring Boot + Kotlin。目前,我们正在将新项目统一使用Go这套技术栈,因为Go不仅满足了我们的需求,还非常易于维护。在数据库方面,我个人对SQL和分布式事务更熟悉,也能够在不同的业务场景中使用不同的数据库和工具。

我加入公司之初,我们的服务和服务器都是单体和独立的,各种技术栈和老项目混乱无章,也恰逢人员交接之际。然而,随着新业务的展开,我们从零开始,大量地构建和改进,同时采用了K8S。如今,无论是服务文档服务框架单元测试,还是CI/CD,我们都已经运行得非常成熟,这也非常适合我们目前和未来一段时间的项目架构。

我始终认为技术人应该持续学习和进步。我觉得我已经做到了我能做到的事情,至于更多的东西,我知道我现在的知识储备可能有限,但我仍希望能更进一步。年轻的我没有太多负担,希望能够抓住机会,走得更远。这就是我正在寻找新机会的原因。我期待挑战,也期待更有趣的工作。我相信,无论结果如何,这个过程都会带给我宝贵的经验和成长。

]]>
职业道路上的新旅程:一个游戏开发者的自我介绍
使用GitHub Action 同步运动数据并生成热力图 github-poster 2023-06-04T21:14:48+08:00 2023-06-04T21:14:48+08:00

如果你想把你的运动数据和热力图同步到 GitHub,那么你来对地方了。在这篇文章中,我将详细解释如何使用 GitHub Actions 和 Python 自动同步和更新你的数据。

这个项目是在伊洪@yihong0618的GitHubPoster的基础上进行的所以在此表示感谢。他的另一个项目是IBeat (之前我对GitHub Action做了一次尝试),从这里我开始接触了快捷指令的触发。

最终效果如下:

我的运动数据

一. 开始前的准备

首先,你需要 fork @yihong0618的项目,并将其克隆到本地: https://github.com/whrsss/GitHubPoster

然后,安装项目的依赖项:


pip3 install -r requirements.txt

二. 全量生成历史数据

研究了一下,步骤是先全量生成历史数据(用全量模式backfill),再进行追加(incremental)。

下面是如何导出每日运动量(卡路里)出来的例子。首先,将apple 运动中的数据导出,解压在IN_FOLDER文件夹中,然后在项目根目录运行:


python3 -m github_poster apple_health --apple_health_mode backfill --year 2020-2023 --apple_health_record_type move --me "your name"

这样在OUT_FOLDER目录下就会生成一份全量的数据,IN_FOLDER下也会生成对应的全量json文件,之后的增量,会加入到IN_FOLDER的json文件中。

逻辑微调

由于我的目标是统计每日消耗的卡路里,在apple health导出的数据中,分成了两块儿, 分别是

type=“HKQuantityTypeIdentifierBasalEnergyBurned”和 type=“HKQuantityTypeIdentifierActiveEnergyBurned” ,每日基础消耗数据和每日运动活动数据。

(ps. Apple 运动记录中不同的运动类型在这里,想要自定义其他可以自取)

而在GitHubPoster中支持的3种类型中,move、exercise、stand,对应的apple health数据都是单种类型,所以,需要将type修改为数组类型才能实现。

在 github_poster/loader/apple_health_loader.py 中修改其中的 RecordMetadata 的支持和使用。

首先需要将type字段修改为一个列表或者集合。在定义RecordMetadata时,将type字段类型改为ListSet


from typing import List



RecordMetadata = namedtuple("RecordMetadata", ["types", "unit", "track_color", "func"])

然后在定义HEALTH_RECORD_TYPES时,你就可以为每个键提供多个类型了:


HEALTH_RECORD_TYPES = {

    "stand": RecordMetadata(

        ["HKCategoryTypeIdentifierAppleStandHour", "AnotherType", ...],

        "hours",

        "#62F90B",

        lambda x: 1 if "HKCategoryValueAppleStandHourStood" else 0,

    ),

    ...

}

接着,你需要在AppleHealthLoader类的backfill方法中,修改判断记录类型的逻辑。将原来的等于比较改为检查类型是否在types列表中:


def backfill(self):

    from_export = defaultdict(int)



    in_target_section = False

    for _, elem in ET.iterparse(self.apple_health_export_file, events=["end"]):

        if elem.tag != "Record":

            continue



        if elem.attrib["type"] in self.record_metadata.types:

            ...

这样,每个RecordMetadata就可以包含多个type了,可以在HEALTH_RECORD_TYPES中为每个键定义任意数量的类型。

再次执行,即可生成想要的数据。

到这里,如果你不需要每日更新,只发个朋友圈啥的,就够了。


三. 设置 GitHub Actions 进行数据的增量更新

要实现数据的每日更新,我们需要利用快捷指令和 GitHub Actions。具体的步骤如下:

生成一个AccessToken

我们首先需要生成一个AccessToken,用来作为快捷指令post请求的凭证: image.png

配置GitHub仓库的权限

接下来,我们需要打开GitHub action 的仓库读写权限,用来作为图片生成完成以及数据增量的仓库保存: image.png

编写 GitHub Actions workflow 编写 GitHub Actions 工作流程

我们在.github/workflow目录下创建一个yml后缀的文件,比如叫sync_exercise.yml,下面是我的文件的内容:


name: Run Poster Generate

on:

  workflow_dispatch:

    inputs:

      time:

        description: 'time list'

        required: false

      value:

        description: 'value list'

        required: false

  pull_request:

  

env:

  TYPE: "apple_health" 

  ME: whrsss

  # change env here

  GITHUB_NAME: whrsss

  GITHUB_EMAIL: xxx@qq.com

jobs:

  sync:

    name: Sync

    runs-on: ubuntu-latest

    steps:

      - name: Checkout

        uses: actions/checkout@v3

      - name: Set up Python

        uses: actions/setup-python@v4

        with:

          python-version: 3.8



      - name: Install dependencies

        run: |

          python -m pip install --upgrade pip

          pip install -r requirements.txt

        if: steps.pip-cache.outputs.cache-hit != 'true'



      - name: Run sync apple_health script

        if: contains(env.TYPE, 'apple_health')

        run: |

          python3 -m github_poster apple_health --apple_health_mode incremental --apple_health_date ${{ github.event.inputs.time }} --apple_health_value ${{ github.event.inputs.value }} --year 2020-2023 --apple_health_record_type move --me "whrsss"



      - name: Push new poster

        run: |

          git config --local user.email "${{ env.GITHUB_EMAIL }}"

          git config --local user.name "${{ env.GITHUB_NAME }}"

          git add .

          git commit -m 'update new poster' || echo "nothing to commit"

          git push || echo "nothing to push"



如果想要了解github action语法的同学可以google一下

上面的yaml的步骤分别是:

  1. 定义了一个名为 “Run Poster Generate” 的 Github Actions

  2. 指定了两种触发方式,一种是手动触发的 workflow_dispatch,另一种是当有新的 pull_request 时触发

  3. 指定了一些环境变量,包括 poster 生成所需的参数

  4. 定义了一个名为 “sync” 的 job,指定在 ubuntu-latest 操作系统上运行

  5. 包括了三个步骤:

    • checkout:将代码仓库从 Github 上下载到本地

    • set up python:安装 python 3.8 版本

    • install dependencies:安装项目所需的所有依赖包

  6. 如果缓存中没有依赖,才会继续执行后续步骤

  7. 如果环境变量中的 TYPE 是 apple_health,则执行同步 Apple Health 数据的脚本

  8. 完成同步后,将生成的新 poster 推送到 Github 仓库中去。

获取workflow id 获取workflow id

我们可以通过以下命令获取workflow id:


curl https://api.github.com/repos/{用户名}/{仓库名}/actions/workflows -H "Authorization: token d8xxxxxxxxxx" # change to your config

上面的token后的token就是第一步获取的token。

image.png

创建快捷指令

你可以下载大佬已经创建好的快捷指令,然后修改以下部分即可:

设置自动化

最后,我们需要设置快捷指令的自动触发条件。我选择的是每天结束时进行同步: 54781685519843_.pic.jpg

结束语

希望这篇文章对你有所帮助。如果你试过这个项目,我很乐意听到你的反馈和经验。如果你有任何问题或建议,也欢迎在下面的评论中提出。

]]>
如果你想把你的运动数据和热力图同步到 GitHub,那么你来对地方了。在这篇文章中,我将详细解释如何使用 GitHub Actions 和 Python 自动同步和更新你的数据。 这个项目是在伊洪@yihong0618的GitHubPoster的基础上进行的所以在此表示感谢。他的另一个项目是IBeat (之前我对GitHub Action做了一次尝试),从这里我开始接触了快捷指令的触发。
【Gorm】Save 方法更新踩坑记录 gorm-save-issue 2023-06-03T16:53:55+08:00 2023-06-03T16:53:55+08:00

在我最近使用Gorm进行字段更新的过程中,我遇到了一个问题。当我尝试更新status字段时,即使该字段的值没有发生变化,Gorm还是提示我“Duplicate entry ‘xxxx’ for key ‘PRIMARY’”。

首先,让我们看看Gorm的官方文档对Save方法的描述:

Save方法会保存所有的字段,即使字段是零值。


db.First(&user)  



user.Name = "jinzhu 2"  

user.Age = 100  

db.Save(&user)  

// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111;  



```  



`Save`方法是一个复合函数。如果保存的数据不包含主键,它将执行`Create`。反之,如果保存的数据包含主键,它将执行`Update`(带有所有字段)。



```go

db.Save(&User{Name: "jinzhu", Age: 100})  

// INSERT INTO `users` (`name`,`age`,`birthday`,`update_at`) VALUES ("jinzhu",100,"0000-00-00 00:00:00","0000-00-00 00:00:00")  



db.Save(&User{ID: 1, Name: "jinzhu", Age: 100})  

// UPDATE `users` SET `name`="jinzhu",`age`=100,`birthday`="0000-00-00 00:00:00",`update_at`="0000-00-00 00:00:00" WHERE `id` = 1  



根据这个描述,我预期的行为应该是更新操作,因为我提供了ID字段。然而,实际发生的却是插入操作。这让我感到困惑。

为了理解这个问题,我深入阅读了Gorm的源码:


// Save updates value in database. If value doesn't contain a matching primary key, value is inserted.func (db *DB) Save(value interface{}) (tx *DB) {  

tx = db.getInstance()  

tx.Statement.Dest = value  

  

reflectValue := reflect.Indirect(reflect.ValueOf(value))  

for reflectValue.Kind() == reflect.Ptr || reflectValue.Kind() == reflect.Interface {  

reflectValue = reflect.Indirect(reflectValue)  

}  

  

switch reflectValue.Kind() {  

case reflect.Slice, reflect.Array:  

if _, ok := tx.Statement.Clauses["ON CONFLICT"]; !ok {  

tx = tx.Clauses(clause.OnConflict{UpdateAll: true})  

}  

tx = tx.callbacks.Create().Execute(tx.Set("gorm:update_track_time", true))  

case reflect.Struct:  

if err := tx.Statement.Parse(value); err == nil && tx.Statement.Schema != nil {  

for _, pf := range tx.Statement.Schema.PrimaryFields {  

if _, isZero := pf.ValueOf(tx.Statement.Context, reflectValue); isZero {  

return tx.callbacks.Create().Execute(tx)  

}  

}  

}  

  

fallthrough  

default:  

selectedUpdate := len(tx.Statement.Selects) != 0  

// when updating, use all fields including those zero-value fields  

if !selectedUpdate {  

tx.Statement.Selects = append(tx.Statement.Selects, "*")  

}  

  

updateTx := tx.callbacks.Update().Execute(tx.Session(&Session{Initialized: true}))  

  

if updateTx.Error == nil && updateTx.RowsAffected == 0 && !updateTx.DryRun && !selectedUpdate {  

return tx.Create(value)  

}  

  

return updateTx  

}  

  

return  

}

源码的主要逻辑如下:

  1. 获取数据库实例并准备执行SQL语句。value是要操作的数据。

  2. 利用反射机制确定value的类型。

  3. 如果value是Slice或Array,并且没有定义冲突解决策略(”ON CONFLICT”),那么设置更新所有冲突字段的冲突解决策略,并执行插入操作。

  4. 如果value是一个Struct,那么会尝试解析这个结构体,然后遍历它的主键字段。如果主键字段是零值,则执行插入操作。

  5. 对于除Slice、Array、Struct以外的类型,将尝试执行更新操作。如果在更新操作后,没有任何行受到影响,并且没有选择特定的字段进行更新,则执行插入操作。

从这个函数我们可以看出,当传入的value对应的数据库记录不存在时(根据主键判断),Gorm会尝试创建一个新的记录。如果更新操作不影响任何行,Gorm同样会尝试创建一个新的记录。

这个行为与我们通常理解的“upsert”(update + insert)逻辑略有不同。在这种情况下,即使更新的数据与数据库中的数据完全相同,Gorm还是会尝试进行插入操作。这就是为什么我会看到Duplicate entry 'xxxx' for key 'PRIMARY'的错误,因为这就是主键冲突的错误提示。

对于Gorm的这种行为我感到困惑,同时我也对官方文档的描述感到失望,因为它并没有提供这部分的信息。

如何解决这个问题呢?

我们可以自己实现一个Save方法,利用GORM的Create方法和冲突解决策略:


// Update all columns to new value on conflict except primary keys and those columns having default values from sql func  

db.Clauses(clause.OnConflict{  

  UpdateAll: true,  

}).Create(&users)  

// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age", ...;  

// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age`=VALUES(age), ...; MySQL



在Gorm的Create方法的文档中,我们可以看到这种用法。如果提供了ID,它会更新其他所有的字段。如果没有提供ID,它会插入新的记录。


经热心老哥提醒,当使用 INSERT ... ON DUPLICATE KEY UPDATE 语句时,如果插入的行因为唯一索引或主键冲突而失败,MySQL 会执行更新操作。这种情况下,会涉及到锁的问题,如果多个事务同时试图对同一行进行 INSERT ... ON DUPLICATE KEY UPDATE 操作,可能会引起死锁,尤其是在复杂的查询和多表操作中。死锁发生时,MySQL 会自动检测并回滚其中一个事务来解决死锁。频繁使用这种语句在高并发场景下可能会对性能造成影响,因为每次冲突都需要进行额外的更新操作,并且涉及到锁的管理。

所以,如果你的业务涉及复杂和并发的业务场景,可以尝试手动在应用层进行检测冲突,避免数据库的冲突和锁的竞争。

]]>
在我最近使用Gorm进行字段更新的过程中,我遇到了一个问题。当我尝试更新status字段时,即使该字段的值没有发生变化,Gorm还是提示我“Duplicate entry 'xxxx' for key 'PRIMARY'”。
一次线上异常的追踪与处理 exception-tracking 2023-06-02T19:24:29+08:00 2023-06-02T19:24:29+08:00

一次线上异常的追踪与处理

5月31日晚,我们接到游戏玩家反馈,经常出现请求超时的提示。在我亲自登录游戏验证后,也出现了相同的错误,但游戏仍然可以正常运行,数据也没有任何问题。

经过客户端的错误检查,我们发现请求出现了408 Request Timeout的错误。该响应状态码意味着服务器打算关闭没有在使用的连接,即使客户端没有发送任何请求,一些服务器仍会在空闲连接上发送此信息。服务器决定关闭连接,而不是继续等待。

1. 日志检查

接下来,我查看了服务器的日志,发现后台的两个服务的日志都在正常运行,没有异常提示。当我进行pod查看时,发现有两个pod显示容器没有日志,这两个pod已经挂掉。

为什么这两个pod会宕机呢?我开始回溯近1小时的日志,发现在晚上10点左右,出现了JDBC连接异常。


### Error querying database. Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 31363ms.

通过Google查询,我了解到这种错误是由于Spring Boot的默认连接池HikariPool在连接排队阻塞,无法获取连接,最后导致超时。在数据库错误之后的一段时间内,出现了Java内存异常。


{"@timestamp":"2023-05-31 22:18:24.382","level":"ERROR","source":{"className":"org.apache.juli.logging.DirectJDKLog","methodName":"log","line":175},"message":"Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Java heap space] with root cause","error.type":"java.lang.OutOfMemoryError","error.message":"Java heap space","error.stack_trace":"java.lang.OutOfMemoryError: Java heap space\n"}

由于我们没有设置连接池上限(默认最大为10),当获取连接阻塞后,请求排队,最终导致内存溢出。最后,由于内存溢出,pod触发java.io.IOException: Broken pipe错误,即管道断开,服务宕机。


{"@timestamp":"2023-05-31 22:18:24.393","level":"WARN","source":{"className":"org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver","methodName":"logException","line":199},"message":"Resolved [org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Java heap space]"}

2. 增加连接池大小

Hikari是Spring Boot自带的连接池,默认最大只有10个。因此,我的第一步解决方案是增加这个服务的连接池大小。在服务的yaml数据库连接配置中增加了一些参数。 


  datasource:

    url: 'jdbc:mysql://rm-2xxxxxx'

    username: 'xx'

    password: 'xxx'

    # 下面这些????

    type: com.zaxxer.hikari.HikariDataSource

    driver-class-name: com.mysql.cj.jdbc.Driver

    hikari:

      #连接池名

      pool-name: DateHikariCP

      #最小空闲连接数

      minimum-idle: 10

      # 空闲连接存活最大时间,默认600000(10分钟)

      idle-timeout: 180000

      # 连接池最大连接数,默认是10

      maximum-pool-size: 100

      # 此属性控制从池返回的连接的默认自动提交行为,默认值:true

      auto-commit: true

      # 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟

      max-lifetime: 1800000

      # 数据库连接超时时间,默认30秒,即30000

      connection-timeout: 30000

      connection-test-query: SELECT 1



3. 性能优化

尽管从表面上看,问题是由于连接池数量太少,导致连接请求阻塞。但深层的原因是服务对数据库的请求处理过慢,最后导致阻塞。如果请求数量继续增加,即使扩大了连接池,同样会阻塞连接。这就像滴滴打车,碰到下雨天儿,队一旦开始排,后面就不知道要排多久了。

这个数据库存储了大量的数据,其中聊天记录的存储主要占用了性能。我们在处理聊天记录时做了分表处理。但由于数据量过大,单表依然有近两千万的数据。这张大表有一个联合索引,索引数据量较大。每次更新都需要维护索引空间,每次单个玩家数据量到达限值,就会进行局部清理。

这里的数据插入动作可能消耗时间较长。由于对消息的可靠性要求不高,我们可以使用异步进行,这样在等待插入的过程中可以省去大量的请求连接占用资源。

我们优化了消息保存数量。以前,每个玩家保存900条消息,但一般只查询最近的300条。现在,每500条进行一次清理,清理至300条,以节省空间。

即使数据量节省了很多,但由于业务价值相对成本比例因素,与业务部门进行沟通,将业务的容忍度定为定期3个月。

4. 空间优化

查询表空间占用:


-- 查询库中每个表的空间占用,分项列出 

select   table_schema as '数据库',   table_name as '表名',   table_rows as '记录数',   truncate(data_length/1024/1024, 2) as '数据容量(MB)',   truncate(index_length/1024/1024, 2) as '索引容量(MB)'   

from information_schema.tables   

where table_schema='表名'   

order by data_length desc, index_length desc;

对经常有删除操作的数据表进行碎片清理:


alter table 表名 engine=innodb;

经过清理,可以看到表空间占用缩小了40%左右。加上之前的业务修改,数据量又有了明显的缩减,使得数据库到了MySQL的舒适区,单表在500万左右。

5. 相关经验

以前我们有一个Go服务有非常大的IO,偶尔会出现崩溃,日志也是提示:“write tcp IP: xxx-> IP:xxx write: broken pipe”。开始以为是服务器在上传到OSS的过程中出现的连接异常,后来和阿里确认了并非OSS的断开错误。经过多次排查,最后发现在上传文件前,对内容进行了json序列化,这个过程非常费性能。当请求过多时,就发生了阻塞,阻塞过多,内存占用过大,溢出,服务就会拒绝服务。此时,连接的管道就会强行断开。

在很多业务场景中,都会出现这种情况:当计算资源不足时,请求就会阻塞堆积,最后最先崩溃的总是内存。

]]>
5月31日晚,我们接到游戏玩家反馈,经常出现请求超时的提示。在我亲自登录游戏验证后,也出现了相同的错误,但游戏仍然可以正常运行,数据也没有任何问题。 经过客户端的错误检查,我们发现请求出现了`408 Request Timeout`的错误。该响应状态码意味着服务器打算关闭没有在使用的连接,即使客户端没有发送任何请求,一些服务器仍会在空闲连接上发送此信息。服务器决定关闭连接,而不是继续等待。
Go 单元测试高效实践 go-test 2023-04-21T18:27:10+08:00 2023-04-21T18:27:10+08:00

敏捷开发中有一个广为人知的开发方法就是 XP(极限编程),XP 提倡测试先行,为了将以后出现 bug 的几率降到最低,这一点与近些年流行的 TDD(测试驱动开发)有异曲同工之处。

在最开始做编程时,我总是忽略单元测试在代码中的作用,觉得编写单元测试的功夫都赶上甚至超越业务程序了。到后来,业务量越来越复杂,慢慢地,浮现一个问题,就是系统对于测试人员是一个黑盒,简单的测试无法保证系统所设计的东西都可以测试到⬇️

举两个最简单的例子:

系统设计的数据打点,是无法从功能业务上测试出来的,而对于测试人员,可能由于版本差异,用例未覆盖。

如果一个表中有两个字段,新用户过来更新一个字段之后,测另一个字段的功能时就不再以一个新用户的身份操作了。

在这样的情况下,如果开发人员没有对系统做完全的检查,就很可能出现问题。

就以上情况看,需要从开发人员的维度,对功能做一个“预期”测试,一个功能走过,应该输入什么,输出什么,哪些数据变动了,变动是否符合预期等等。

最近,公司业务基本都转入了 Go 做开发,在 Go 的整个业务处理上也日渐完善,而 Go 的单元测试用起来也十分顺手,所以做个小的总结。

一. Mock DB

在单元测试中,很重要的一项就是数据库的 Mock,数据库要在每次单元测试时作为一个干净的初始状态,并且每次运行速度不能太慢。

1. Mysql 的 Mock

这里使用到的是 github.com/dolthub/go-mysql-server 借鉴了这位大哥的方法 如何针对 MySQL 进行 Fake 测试

  • ###### DB 的初始化

在 db 目录下


type Config struct {

   DSN             string // write data source name.

   MaxOpenConn     int    // open pool

   MaxIdleConn     int    // idle pool

   ConnMaxLifeTime int

}



var DB *gorm.DB



// InitDbConfig 初始化Db

func InitDbConfig(c *conf.Data) {

   log.Info("Initializing Mysql")

   var err error

   dsn := c.Database.Dsn

   maxIdleConns := c.Database.MaxIdleConn

   maxOpenConns := c.Database.MaxOpenConn

   connMaxLifetime := c.Database.ConnMaxLifeTime

   if DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{

      QueryFields: true,

      NamingStrategy: schema.NamingStrategy{

         //TablePrefix:   "",   // 表名前缀

         SingularTable: true, // 使用单数表名

      },

   }); err != nil {

      panic(fmt.Errorf("初始化数据库失败: %s \n", err))

   }

   sqlDB, err := DB.DB()

   if sqlDB != nil {

      sqlDB.SetMaxIdleConns(int(maxIdleConns))                               // 空闲连接数

      sqlDB.SetMaxOpenConns(int(maxOpenConns))                               // 最大连接数

      sqlDB.SetConnMaxLifetime(time.Second * time.Duration(connMaxLifetime)) // 单位:秒

   }

   log.Info("Mysql: initialization completed")

}

  • ###### fake-mysql 的初始化和注入

在 fake_mysql 目录下


var (

   dbName    = "mydb"

   tableName = "mytable"

   address   = "localhost"

   port      = 3380

)



func InitFakeDb() {

   go func() {

      Start()

   }()

   db.InitDbConfig(&conf.Data{

      Database: &conf.Data_Database{

         Dsn:             "no_user:@tcp(localhost:3380)/mydb?timeout=2s&readTimeout=5s&writeTimeout=5s&parseTime=true&loc=Local&charset=utf8,utf8mb4",

         ShowLog:         true,

         MaxIdleConn:     10,

         MaxOpenConn:     60,

         ConnMaxLifeTime: 4000,

      },

   })

   migrateTable()

}



func Start() {

   engine := sqle.NewDefault(

      memory.NewMemoryDBProvider(

         createTestDatabase(),

         information_schema.NewInformationSchemaDatabase(),

      ))



   config := server.Config{

      Protocol: "tcp",

      Address:  fmt.Sprintf("%s:%d", address, port),

   }



   s, err := server.NewDefaultServer(config, engine)

   if err != nil {

      panic(err)

   }



   if err = s.Start(); err != nil {

      panic(err)

   }



}



func createTestDatabase() *memory.Database {

   db := memory.NewDatabase(dbName)

   db.EnablePrimaryKeyIndexes()

   return db

}



func migrateTable() {

// 生成一个user表到fake mysql中

   err := db.DB.AutoMigrate(&model.User{})

   if err != nil {

      panic(err)

   }

}

在单元测试开始,调用 InitFakeDb() 即可


func setup() {

   fake_mysql.InitFakeDb()

}


2. Redis 的 Mock

这里用到的是 miniredis , 与之配套的Redis Client 是 go-redis/redis/v8 ,在这里调用 InitTestRedis() 注入即可


// RedisClient redis 客户端  

var RedisClient *redis.Client  

  

// ErrRedisNotFound not exist in redisconst ErrRedisNotFound = redis.Nil  

  

// Config redis config

type Config struct {  

   Addr         string  

   Password     string  

   DB           int  

   MinIdleConn  int  

   DialTimeout  time.Duration  

   ReadTimeout  time.Duration  

   WriteTimeout time.Duration  

   PoolSize     int  

   PoolTimeout  time.Duration  

   // tracing switch  

   EnableTrace bool  

}  

  

// Init 实例化一个redis client  

func Init(c *conf.Data) *redis.Client {  

   RedisClient = redis.NewClient(&redis.Options{  

      Addr:         c.Redis.Addr,  

      Password:     c.Redis.Password,  

      DB:           int(c.Redis.DB),  

      MinIdleConns: int(c.Redis.MinIdleConn),  

      DialTimeout:  c.Redis.DialTimeout.AsDuration(),  

      ReadTimeout:  c.Redis.ReadTimeout.AsDuration(),  

      WriteTimeout: c.Redis.WriteTimeout.AsDuration(),  

      PoolSize:     int(c.Redis.PoolSize),  

      PoolTimeout:  c.Redis.PoolTimeout.AsDuration(),  

   })  

  

   _, err := RedisClient.Ping(context.Background()).Result()  

   if err != nil {  

      panic(err)  

   }  

  

   // hook tracing (using open telemetry)  

   if c.Redis.IsTrace {  

      RedisClient.AddHook(redisotel.NewTracingHook())  

   }  

  

   return RedisClient  

}  

  

// InitTestRedis 实例化一个可以用于单元测试的redis  

func InitTestRedis() {  

   mr, err := miniredis.Run()  

   if err != nil {  

      panic(err)  

   }  

   // 打开下面命令可以测试链接关闭的情况  

   // defer mr.Close()  

  

   RedisClient = redis.NewClient(&redis.Options{  

      Addr: mr.Addr(),  

   })  

   fmt.Println("mini redis addr:", mr.Addr())  

}

二. 单元测试

经过对比,我选择了 goconvey 这个单元测试框架

它比原生的go testing 好用很多。goconvey还提供了很多好用的功能:

  • 多层级嵌套单测

  • 丰富的断言

  • 清晰的单测结果

  • 支持原生go test

使用


go get github.com/smartystreets/goconvey


func TestLoverUsecase_DailyVisit(t *testing.T) {  

   Convey("Test TestLoverUsecase_DailyVisit", t, func() {  

      // clean  

      uc := NewLoverUsecase(log.DefaultLogger, &UsecaseManager{})  

  

      Convey("ok", func() {  

         // execute  

         res1, err1 := uc.DailyVisit("user1", 3)  

         So(err1, ShouldBeNil)  

         So(res1, ShouldNotBeNil)  

         // 第 n (>=2)次拜访,不应该有奖励,也不应该报错  

         res2, err2 := uc.DailyVisit("user1", 3)  

         So(err2, ShouldBeNil)  

         So(res2, ShouldBeNil)  

      })  

   })  

}

可以看到,函数签名和 go 原生的 test 是一致的

测试中嵌套了两层 Convey,外层new了内层Convey所需的参数

内层调用了函数,对返回值进行了断言

这里的断言也可以像这样对返回值进行比较 So(x, ShouldEqual, 2)

或者判断长度等等 So(len(resMap),ShouldEqual, 2)

Convey的嵌套也可以灵活多层,可以像一棵多叉树一样扩展,足够满足业务模拟。


三. TestMain

为所有的 case 加上一个 TestMain 作为统一入口


import (  

"os"  

"testing"  

  

. "github.com/smartystreets/goconvey/convey"  

)  

  

func TestMain(m *testing.M) {  

   setup()  

   code := m.Run()  

   teardown()  

   os.Exit(code)

}

// 初始化fake db

func setup() {  

   fake_mysql.InitFakeDb()  

   redis.InitTestRedis()

}

]]>
敏捷开发中有一个广为人知的开发方法就是 XP(极限编程),XP 提倡测试先行,为了将以后出现 bug 的几率降到最低,这一点与近些年流行的 TDD(测试驱动开发)有异曲同工之处。 在最开始做编程时,我总是忽略单元测试在代码中的作用,觉得编写单元测试的功夫都赶上甚至超越业务程序了。到后来,业务量越来越复杂,慢慢地,浮现一个问题,就是系统对于测试人员是一个黑盒,简单的测试无法保证系统所设计的东西都可以测试到。
一个有趣且实用的开源项目,将网页打包成应用 pake 2023-04-04T16:19:46+08:00 2023-04-04T16:19:46+08:00

最近在GitHub上面发现了一个很有趣的开源应用,可以将网页打包成应用:

这里是它的使用文档

底层使用了rust进行开发,支持 Mac、Windows 和 Linux。使用命令将网站直接打包成应用。

这里我以最近部署的一个私人 ChatGPT Next为例, 我的地址为 https://gpt.whrss.com

需要本地先有一个node环境,能使用npm命令。

然后npm全局安装这个工具


npm install -g pake-cli

安装完之后,就可以使用


pake url [OPTIONS]...

来生成桌面应用了,参数有很多,我先用了这些:

这是我的命令:


pake https://gpt.whrss.com --name GPT  --icon /Users/wu/Downloads/gpt.icns --height 700 --width 900

这里我指定了网站的url : https://gpt.whrss.com

指定了生成的应用包的名字:GPT

指定了应用的 icon

指定了应用打开初始化的高度和宽度

mac icon生成和获取的地方 : https://macosicons.com/#/

然后我进行了运行,因为我的icon指定的目录是Downloads下面,所以它直接生成在了下面,默认是用户目录。

image.png

如上图,生成的包体非常的小和轻量,只有不到3M。

= 更新 =


# 查看npm安装的插件的版本

npm -g list

/usr/local/lib

├── xxx

├── npm@9.3.1

├── pake-cli@2.0.0-alpha7

├── xxxxxxx

└── xxxxxxx



# 更新指定插件

npm -g update pake-cli 

# 再次查看 

npm -g list


关于这个软件的应用,我想到的最多就是那些常用的,但没有pc客户端的应用,比如 Twitter、WeRead、Spotify 等等,我也是接触这个项目后第一时间就使用上了,体验很不错,比之前方便了不少。

image.png

]]>
最近在GitHub上面发现了一个[很有趣的开源应用](https://github.com/tw93/Pake),可以将网页打包成应用 底层使用了rust进行开发,支持 Mac、Windows 和 Linux。使用命令将网站直接打包成PC应用。
GitHub Action 自动化部署简单尝试 github-action 2023-03-31T18:38:20+08:00 2023-03-31T18:38:20+08:00

之前看到很多大佬的 Blog 是部署在 Github 上面的,但因为自己目前的博客是带后端的,所以就没有考虑。很久之前看到 @yihong 的心跳和跑步,感觉挺不错的,但因为自己没有跑步的习惯,就感觉不是很感冒 ???? 直到最近在听零机一动的时候,又听到了 yihong 的跑步, 我突然想到,我应该也可以把我的游泳 骑车 有氧也像 @yihong 的跑步数据一样上传过来。那么第一件事,我需要了解一下 GitHub Action 的机制,小小地尝试一下。


首先,先看下 github action 的基本功能,我是通过阮一峰老师的文章了解的。

GitHub Actions 是一种自动化工具,2019 年开始测试,同年 11 月上线。可以在 GitHub 上构建、测试和部署软件项目。它允许 Github 用户通过预定义或自定义的操作序列来自动化整个软件开发生命周期中的流程(过程很像 Dockerfile 或 Jenkinsfile 的构建),并且完全与 Github 集成。

使用 GitHub Actions,你可以创建一个由事件触发或定期运行的工作流(workflow),其中包含一个或多个步骤(steps)。每个步骤都是一个特定任务的命令,例如编译代码、运行测试、打包发布版本等。你可以选择将这些步骤放在同一个工作流内,也可以将它们拆分为不同的工作流文件。

此外,Github Action 还提供了各种操作 (actions),可以让您轻松地执行常见的任务,如 Shell 脚本、推送 Docker 映像、调用 API 等等。 也有一些和捷径市场一样的分享社区,把自己编写的 action yml 分享给他人使用。

GitHub Actions 使得整个开发过程变得更加高效方便,能够提升团队的交付速度。同时,近年来越来越多的开源社区也开始尝试在其上面构建 CI/CD 流水线。

这里可能没有接触过的朋友可能会想,我是否可以在上面部署应用呢?

这里的 Action 的作用类似于 Jenkins,可以帮你打包编译,如果你是一个静态项目,那么作为静态仓库的 Github 仓库是可以直接部署的; 但是如果是动态的后端服务,需要 Cpu 的,就无法进行了。


第一步,创建 GitHub Token

头像 -> settings -> 左侧 Developer settings -> 左侧 Personal access tokens -> Tokens(classic)

-> Generate new token (classic)

赋予 workflow 的权限

第二步: 在目标仓库中创建  .github/workflows  目录

目录中创建一个 yml 文件 workflow-name.yml

按照 GitHub 文档进行 yml 编写,我这里先了解机制,所以直接 fork @yihong 的项目,

第三步 Action 设置

在项目的 settings 中找到 actions 下的 general,向下划动

image.png

image.png

找到工作流的读写权限,进行勾选。这样我们在触发 action 后,项目的 push 就有权限进行了。

第四步,触发这个 Action

在手机上部署这个捷径

Token 填写第一步获取到的Token

GitHub_Name 填写Github用户名

Action_ID 填写yml名字(workflow-name.yml就全部填上去)

修改仓库名字和分支(图中的whrsss是我的仓库名字,master是我的分支名)

image.png

修改完成后,点击快捷指令的运行。然后打开Github 仓库,点开 Action 栏,项目正在构建。点开可以查看构建日志。

image.png

]]>
之前看到很多大佬的 Blog 是部署在 Github 上面的,但因为自己目前的博客是带后端的,所以就没有考虑。很久之前看到 @yihong 的心跳和跑步,感觉挺不错的,但因为自己没有跑步的习惯,就感觉不是很感冒 ???? 直到最近在听零机一动的时候,又听到了 yihong 的跑步, 我突然想到,我应该也可以把我的游泳 骑车 有氧也像 @yihong 的跑步数据一样上传过来。那么第一件事,我需要了解一下 GitHub Action 的机制,小小地尝试一下。
从零开始搭建家庭软路由系统(安装OpenWrt,并做为旁路由接入家庭网络) soft-routes 2023-03-21T14:09:57+08:00 2023-03-21T14:09:57+08:00

之前刷推看到了不少人发软路由,最近又看到a姐发了一句:全推入手软路由了

开始我还觉得软路由对我的作用应该不大吧,随着从众心理的影响,我觉得我应该试试。

刚好我还有从笔记本上拆下的内存和固态,岂不是严丝合缝?

然后我就下单了一个50091679362219_.pic.jpg

外观差不多是这个样子:

50101679362338_.pic.jpg

50111679362342_.pic.jpg

内部结构大概是这个样子:

50121679362346_.pic.jpg

拥有5个网口、两个 USB 2.0 和两个 USB 3.0 口、HDMI、DP、存储卡位以及两个笔记本内存条槽位、一根 m.2 插槽和一个 SATA 接口,可以说五脏俱全。

我买的是只带电源的,内存条和磁盘需要自己加。

一、第一步 组装机器

  • 打开机器背部的固定螺丝以及后盖,将内存条插入相应槽位

  • 接入 SATA 固态硬盘或 M.2 硬盘,不要装上后盖,以防有问题还需要再拆开。

  • 连接电源,此时路由器将自动开机

  • 开机后如果听到一声响,表示内存识别功能正常。如果听到三声响连续作响就表示出现了问题。我使用的两个内存条,分别在两个槽位上尝试,只有一根(金士顿的骇客神条2400)能在其中一个槽位上被识别。如果两个槽位都无法识别,就只能买同店的内存条了。

50131679363062_.pic.jpg

  • 接下来就是准备安装系统。

二、 制作启动盘

我选择的系统是 OpenWRT/LEDE 在国内的家庭软路由中有着非常高的占有率,拥有海量的软件,和非常强大的生态。同时,OpenWRT 的教程也很丰富详实。

这里我使用的是 KoolShare 固件,内置了非常强大的插件市场。

1. 下载efi

image.png

image.png

找到最新版本进行下载。

image.png

2. 写盘

下载完成后,使用balenaetcher写盘工具进行写盘,

  • 如果没有安装balenaEtcher,首先需要下载并安装。

  • 插入你的U盘,并打开balenaEtcher。

  • 点击“选择镜像”按钮,选择刚才下载的.gz文件。

  • 点击“选择驱动器”按钮,选择你插入的U盘。

  • 点击“写入”按钮,balenaEtcher将开始写入镜像文件到你的U盘。

  • 等待写入过程完成,并在完成后安全地弹出。

三、 安装和设置

1. 安装
  • 将写好的盘插入软路由,接一个显示器和一个键盘给软路由,接上电源,开机。按f11(不同机器不同快捷键)进入快速启动。一切自动执行到终端提示完成,回车,提示出OpenWrt图标。

  • 输入quickstart,1 设置Lan口IP,2 安装OpenWrt,3 重置

  • 选择2安装操作系统。这时候会提示安装位置,如果识别磁盘没有问题,就会让你选择你内部的固态或者sata硬盘。如果提示没有找到硬盘,就是硬盘有问题或者接口接错了(我在使用sata协议的m.2接口固态硬盘时,提示了这个问题,固态硬盘需要选择nvme接口协议的)

  • 几秒后会提示你拔掉U盘,这时候就会自动重启进入系统。

  • 同样的 输入quickstart 选择1 设置lan口IP。可以设置为 192.168.2.6, 子网掩码设置为255.255.255.0(等下会说明用途)

  • 接一根网线从软路由 lan 口到 PC 上,pc打开浏览器,访问 192.168.2.6 ,这时候会进入软路由后台,用户名root 默认密码 password

到这里,安装基本完成,openWrt可以进入后台访问。

2. 设置旁路由

旁路由就是将软路由接入在普通硬路由下,做数据处理。

image.png

  • 在硬路由器控制台上禁用 DHCP 功能,然后记录下其 LAN 口 IP 地址(例如,我的是 192.168.2.5)

  • 在软路由面板的“网络”下,选择“接口”并设置 LAN 口,将 LAN 口 IP 设置为硬路由器 IP 同一网段(也就是192.168.2.6),将网关地址和 DNS 解析地址都设置为硬路由器 LAN 口地址 192.168.2.5 点击保存并应用。

拔下PC网线,将软路由Lan口与硬路由Lan口相连。

电脑连接原先的硬路由wifi网络, 浏览器输入192.168.2.6,正确访问到软路由后台。再登录软路由后台系统,检查网络状况,已连通。

]]>
之前刷推看到了不少人发软路由,最近又看到a姐发了一句:全推入手软路由了 开始我还觉得软路由对我的作用应该不大吧,随着从众心理的影响,我觉得我应该试试。 刚好我还有从笔记本上拆下的内存和固态,岂不是严丝合缝?
Clash 设置国内国外自动分流访问 clash-auto-triage 2023-03-10T17:15:55+08:00 2023-03-10T17:15:55+08:00

Clash 是一款开源的网络代理工具,可以帮助用户实现对网络流量的控制和管理。我使用了很久,但苦于每次访问国内外网络需要手动开关代理,于是我就问了下GPT, 还真就解决了。

如果你也需要设置 Clash 区分国内和国外流量,可以按照以下步骤进行操作:

1. 打开配置文件(windows在右下角):

image.png

image.png

使用 Visual Studio Code 打开 config.yaml

内容是这样的:


#---------------------------------------------------#

## 配置文件需要放置在 $HOME/.config/clash/*.yaml



## 这份文件是clashX的基础配置文件,请尽量新建配置文件进行修改。

## !!!只有这份文件的端口设置会随ClashX启动生效



## 如果您不知道如何操作,请参阅 官方Github文档 https://github.com/Dreamacro/clash/blob/dev/README.md

#---------------------------------------------------#



# (HTTP and SOCKS5 in one port)

mixed-port: 7890

# RESTful API for clash

external-controller: 127.0.0.1:9090

allow-lan: false

mode: rule

log-level: warning



proxies: 



proxy-groups:



rules:

- DOMAIN-SUFFIX,google.com,DIRECT

- DOMAIN-KEYWORD,google,DIRECT

- DOMAIN,google.com,DIRECT

- DOMAIN-SUFFIX,ad.com,REJECT

- GEOIP,CN,DIRECT

- MATCH,DIRECT

2. 在 Clash 配置文件中添加以下规则:


# 添加下面的, 跟在后面

payload:

- DOMAIN-SUFFIX,cn,Direct

- DOMAIN-KEYWORD,geosite,Proxy

- IP-CIDR,10.0.0.0/8,Direct

- IP-CIDR,172.16.0.0/12,Direct

- IP-CIDR,192.168.0.0/16,Direct

- IP-CIDR,127.0.0.0/8,Direct

- IP-CIDR,224.0.0.0/4,Direct

- IP-CIDR,240.0.0.0/4,Direct

- MATCH,Final



3. 每个配置段的作用

然后我问了一下 gpt 这些配置的作用:

  • DOMAIN-SUFFIX,cn,Direct 表示所有以 “.cn” 结尾的域名都直接连接(不通过代理)。

  • DOMAIN-KEYWORD,geosite,Proxy 表示包含关键词 “geosite” 的域名(比如 “www.geosite.com”)都通过代理连接。

  • IP-CIDR,10.0.0.0/8,Direct 表示 IP 地址在 10.0.0.0 - 10.255.255.255 范围内的都直接连接。

  • IP-CIDR,172.16.0.0/12,Direct 表示 IP 地址在 172.16.0.0 - 172.31.255.255 范围内的都直接连接。

  • IP-CIDR,192.168.0.0/16,Direct 表示 IP 地址在 192.168.0.0 - 192.168.255.255 范围内的都直接连接。

  • IP-CIDR,127.0.0.0/8,Direct 表示本地回环地址都直接连接(通常是 127.0.0.1)。

  • IP-CIDR,224.0.0.0/4,Direct 和 IP-CIDR,240.0.0.0/4,Direct 表示多播地址都直接连接。

  • MATCH,Final 表示匹配到这条规则后,后面的规则不会再被匹配,可以理解为中断匹配流程,即该规则为终结规则(Final Rule)。

]]>
Clash 是一款开源的网络代理工具,可以帮助用户实现对网络流量的控制和管理。我使用了很久,但苦于每次访问国内外网络需要手动开关代理,于是我就问了下GPT, 还真就解决了。
将本地服务通过 SSH 代理给外部访问 ssh-proxy-conn 2023-03-09T16:08:49+08:00 2023-03-09T16:08:49+08:00

如何使用 ssh 将本地服务代理给外部访问并保持 SSH 会话的连接性

1. 外部服务器 nginx 配置


server {

    listen     localhost:80;

    server_name  _;

    root         /usr/share/nginx/html;



    # 重要:将请求转发到本地服务

    location / {

        root   /usr/share/nginx/html;

        index  index.html index.htm;

        proxy_pass http://127.0.0.1:10412;

        proxy_set_header Host $host:80;

        proxy_set_header X-Real-IP $remote_addr;

        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_set_header Via "nginx";

    }

}



2. 权限认证

  1. 在外网服务器上运行以下命令以生成公钥:ssh-keygen -o

  2. 将公钥复制到内网服务器上,并添加到 ~/.ssh/authorized_keys

3. 目标内网服务器 ssh 连接

  1. 在本地启动服务并将其监听在端口 8088

  2. 将外网访问的端口 10412 转发到本地端口 8088


nohup ssh -N -v -R 10412:127.0.0.1:8088 root@{外部服务器的外网IP} 2>&1 &

4. 保持会话

  1. 在保持 SSH 会话中,加入以下命令来保持连接

  2. ServerAliveInterval 是指定服务器发送保持连接的数据包的时间(单位:秒)

  3. ServerAliveCountMax 是指定尝试与服务器保持连接的最大次数


nohup ssh -N -v -o ServerAliveInterval=10 -o ServerAliveCountMax=1000 -R 10412:127.0.0.1:8088 root@{外部服务器的外网IP} 2>&1 &

]]>
如何使用 ssh 将本地服务代理给外部访问并保持 SSH 会话的连接性
惊喜又焦虑,AI 技术的发展 the-ai-challenge 2023-03-04T15:48:36+08:00 2023-03-04T15:48:36+08:00

最近,我接触了很多关于AI技术的东西。例如ChatGPT、NewBing、ChatGPT更快的API、stable diffusion、AI语音识别等等。这些技术让我惊喜,也让我感到焦虑。

AI应用日新月异,我的想象力甚至赶不上技术的发展。它们不仅代表了技术的进步,也牵动着就业市场的变革。

我又陷入了意义的怪圈。作为一个软件开发者,我的学习和工作是否还有意义和价值?我的未来会是怎样?如果AI技术可以替代我的工作 ,那么我还能做什么?

如果AI技术只是用于娱乐和教育等领域 ,我或许会感到比较放心,因为我可以成为一个普通的使用者,享受这些技术带来的便利和乐趣,而不必担心它们对我的生活产生过大的影响。

但事实是,AI技术已经渗透到包括软件开发在内各个行业和领域 。AI可以提高软件开发人员的效率和质量 ,也可以拓展软件开发人员的知识和技能 。但同时 ,它们也给软件开发人员带来了更大的竞争压力和更高的要求 。

当然 ,我并不否定AI技术 。它们也有很多优点和潜力 。但是 ,我不能忽视它们带来 的改变和挑战。不知道哪一天,我真的会被AI取代。

后来,我在medium上面看到一篇文章:coding-wont-exist-in-5-years-this-is-why 让我对未来的看法变得理性客观了很多。

AI 驱动的工具将取代人类“编码员”。这些工具将能够比人类更快、更高效地编写和调试代码,而且 成本更低。

如果你所能做的就是写代码,那么你就不是“Engineer”,而是“coder”,你一定会被AI取代。

“幸存下来的不是最强壮的物种,也不是最聪明的物种——它**是最能适应变化的物种。”

*- 查尔斯·达尔文

有了chatGPT这样的工具写代码,只会写代码的人是没有用的。正如工匠能够适应和学习新技能以保持竞争力一样,编码人员将能够通过更多地了解如何使用这些工具来发挥自己的优势来做到这一点。

只有渴望成为房间里最聪明的人,才会担心周围的一切都变得比他聪明。

适应新的方式是痛苦的,但只有活着的人才能感受到这种痛苦——死者甚至感受不到火葬的火光。能利用它的人——将前进,而那些不适应的人将不复存在——it is simple as that.

They Are Coming for You !

]]>
最近,我接触了很多关于AI技术的东西。例如ChatGPT、NewBing、ChatGPT更快的API、stable diffusion、AI语音识别等等。这些技术让我惊喜,也让我感到焦虑。
体验 New Bing:一个比 ChatGPT 更强大、更幽默、更有用的搜索引擎聊天机器人 newbing-vs-chatgpt 2023-03-02T15:43:16+08:00 2023-03-02T15:43:16+08:00

最近在推特上看到很多人在谈论New Bing申请的事情,我也收到了试用通过的邮件。于是我马上打开Edge浏览器体验了一下。

注意:如果你也收到了邀请,但是无法进入聊天页面,请参考这篇文章:https://zhuanlan.zhihu.com/p/605970396

New Bing是一个基于人工智能的搜索引擎聊天机器人。它可以回答各种问题,并且提供相关的链接和信息。它还可以跟你聊天,并且很有幽默感。

我在搜索框输入了一些问题,并且看到了右侧New Bing的回复。我还向上滑动鼠标进入了聊天窗口,在那里我跟New Bing进行了一些对话。其中包括一些荒谬的问题:

6091677668780_.pic_hd.jpg

为了对比New Bing的能力,我还用同样的问题向ChatGPT问了一下。6081677668773_.pic_hd.jpg

从这个问题中可以看出,两个机器人都给出了相对正确的答案。但是New Bing却显得更像一个懂得幽默的人,并且每个专业名词都有引用来源。

以前我曾经问过ChatGPT关于滕王阁序的问题。它告诉我滕王阁序是李白、苏轼、白居易写的。每次问都是不同的答案,但都是错的。今天我又问了同样的问题,它给出了这样的回答:

image.png

现在,它的作者是正确的,但是它的地址、内容描述等等都还是错误的。

同样的问题,bing是怎么回复的呢?

image.png

bing的回复没有任何问题,并且对滕王阁 王勃 骈文 对仗的出处都有标出链接引用。它是从这些答案中汇总得出的。

然后我继续问了几个问题:

image.png

在问到一些想法的问题时,bing的表现明显没有最初好,它在没有引用时,杜撰的内容也是不可信的,比如它说这是滕王阁序的最后一句。

二者 相比,chatGPT的聊天确实是一个Demo级别的产品,而NewBing是一个测试级别的产品。

并且二者在训练的层次和模型版本,应该也有先后之别。

对于普通用户来说,new bing未来应该是更加受欢迎的,加上与Edge的融合,产品体验上应该可以完胜单独的浏览器或者AI Chat。

今天我还听到一个消息,就是GPT的API从3.0升级到了3.5,回复快了数倍,而且还更加便宜。看到hongyi直接拿来接了小爱同学,这两天有时间我也去搞一下子。

]]>
最近在推特上看到很多人在谈论New Bing申请的事情,我也收到了试用通过的邮件。于是我马上打开Edge浏览器体验了一下。
Interesting & Useful 的开源项目 open-source-projects 2023-02-28T15:53:37+08:00 2023-02-28T15:53:37+08:00

| 地址 | 描述 |

| ——————————————————- | ——————————————————————————————————————————————————- |

| https://github.com/lmarzen/esp32-weather-epd | 一个天气显示的墨水瓶项目 |

| https://github.com/AUTOMATIC1111/stable-diffusion-webui | Stable Diffusion 模型的 WebUI 界面。这是一个实现在浏览器上使用的 Stable Diffusion 模型的项目,支持通过文本/图片生成图片、嵌入文本、调整图片大小等功能。 |

| https://github.com/gildas-lormeau/SingleFile | 一键下载网页,能够将网页上的文字、图片等内容,完整地整合到单个 HTML 文件里,支持 Chrome、Firefox、Safari、Microsoft Edge 等主流浏览器。 |

| https://github.com/espanso/espanso | 快捷输入工具 |

| https://github.com/tw93/Pake | 将网页打包成桌面应用,支持Mac Windows Linux |

]]>
分享一些平时遇到的有趣的开源项目
聊天记录存储实践 chat-message-store 2023-02-27T13:53:20+08:00 2023-02-27T13:53:20+08:00

公司的某款游戏在1月初接入微软小冰AI聊天功能。为了保存聊天记录并为后续的统计功能做好准备,决定将聊天记录存放在服务端。最初并不清楚聊天数据量的大小以及玩家对聊天功能的使用情况,所以采用了价格和性能相对宽容的MySQL作为存储介质。

经过大约一个月的运营后,聊天记录表中的数据量已经达到了两千万条。反馈给策划部门后,为控制数据量,决定对聊天记录数量进行限制。每个玩家的每个角色最多只保存200条记录,每个玩家最多可保存3个角色的记录,即每个玩家最多只保存900条聊天记录。服务端的处理逻辑随即修改为每存储300条记录后就清理一次,以确保数据量控制在一定范围内。

根据游戏的新增和玩家留存数据,假设每个玩家平均游戏时间为10天,新增的玩家数在2000至3000之间。因此,每10天会有20000至30000个新的玩家数据,按最高值计算,每10天可能会产生最高2700万条数据,每月最多可能达到5000万条数据。这样计算下来,在一个月后,MySQL可能会无法支持数据的频繁读写操作,需要对聊天记录的存储进行调整。

针对游戏的AVG属性,玩家的生命周期相对较短,因此可以根据玩家的注册时间对其进行划分,从而对玩家数据进行分表存储。具体来说,可以按照玩家注册时间的月份对数据进行划分,并使用相应的表进行数据的存储和查询。这样,每个月份可以相对均匀地承载玩家数据,原本可能达到千万级别的数据量,现在可以控制在百万级别,完全可以解决目前的问题(聊天数据量与新增数据正相关,新增相对稳定)。

接下来需要考虑的是,当一个玩家的生命周期结束后,其数据仍然会一直存储在数据库中。而当新的一年开始,新注册的玩家会继续存储,这样对玩家数据的切片就只是一个临时的措施。解决这个问题的方法是分析玩家在主生命周期结束后的行为,发现这些玩家长时间不会再次登录并进行聊天。因此,可以将这些不活跃的玩家数据迁移到便宜的分布式文件存储中,并且记录迁移标志,删除掉原数据库表中的记录。采用定时任务的方式,这样可以减少对业务的影响。

以上的删除操作,都要注意一点,mysql delete删除操作不会释放表空间。这里需要对表空间进行手动释放。手动释放大表空间是一个比较耗费性能的操作,还会对表数据进行锁定。所以释放操作是需要对实际情况进行设计的。在上面的情况中,我们对数据进行按月操作,可以在一个月后,对上月的数据进行清理。比如 3月初清理1月份的表,因为3月份活跃的玩家大多可能是2月份注册的。可以定在凌晨进行定时删除,对业务影响降到最低。

在2月份,新增了对聊天记录进行标记的操作需求,以进行定期反馈和AI调教。这里我们将标记操作的聊天记录独立出来存储,这样可以方便后续的统计和分析。这样也可以避免将标记操作分散在不同的表中,增加数据处理的复杂性。同时,由于标记操作的数据量不会很大,也可以考虑采用NoSQL数据库或者其他内存型数据库来存储这些数据,以提高查询速度和减少存储成本。

]]>
公司的某款游戏在1月初接入微软小冰AI聊天功能。为了保存聊天记录并为后续的统计功能做好准备,决定将聊天记录存放在服务端。
在 Google 设置静态页面 CDN 加速 google-static-cdn 2023-02-15T16:52:35+08:00 2023-02-15T16:52:35+08:00

在google设置静态页面 CDN加速

一、 创建bucket,设置bucket

https://console.cloud.google.com/storage/browser

  • ##### 创建bucket

image.png

  • ##### 设置bucket公开访问
  1. 在bucket列表中,进入刚创建的bucket。

  2. 选择页面顶部附近的权限标签。

  3. 权限部分中,点击 person_add 授予访问权限按钮。

    此时将显示“授予访问权限”对话框。

  4. 新的主帐号字段中,输入 allUsers

  5. 选择角色下拉列表的过滤条件框中输入 Storage Object Viewer,然后从过滤后的结果中选择 Storage Object Viewer

  6. 点击保存

  7. 点击允许公开访问

二、设置CDN

https://console.cloud.google.com/net-services/cdn/list

添加来源,创建时可能会附带创建好负载均衡,用来做DNS解析使用

image.png

指向刚才新创建的bucket

在host and path rules设置地址规则。

三、DNS解析

进入负载均衡控制台

https://console.cloud.google.com/net-services/loadbalancing/list/loadBalancers

选择创建好的负载均衡,点进去,拿到公网IP,到DNS控制台进行解析即可。

]]>
在google设置静态页面 CDN加速
发现优质 Newsletter 和 Blog find-newsletter 2023-02-05T19:47:18+08:00 2023-02-05T19:47:18+08:00

我平时一直都在发现很多优质的资源,但都没有整理。一直增加,单独一个页面可能开始会有些乱,就先开一篇放在这里。

| 名称 | 简介 |

| ——————————————————————————————— | ——————————-|

| 新趣集 | 发现有趣的新产品 |

| AlleyRead | 发现国内外优质内容 |

| 小众软件 - 发现 | 寻找应用、软件 |

| v2ex - 发现 | 分享好玩的,并获取灵感 (须自强) |

| v1tx | 分享实用工具与解决问题 |

| 炒饭 | 发现有趣的内容| |

| 创造者日报 | 每天发现有趣的产品| |

| producthunt | 每天发现有趣的新产品 (国外产品) |

| 找到 AI | 找到你喜爱的作品 |

| 52 破解 | 找到你想要的软件 |

| 发现优质 Newsletter)| 授人予鱼 不如授人予渔, 这是一个newsletter的推荐网站 | |


Copy By yihong Blog , 都是非常好的博客⬇️

@yihong0618 收藏的博客

| 博客名称 | 添加日期 | type | 备注 |

| ——- | ——- | —- | —- |

| niyue | 2022.01.20 | 第一个 | 写 20 年博客的前辈 |

| baotiao | 2022.01.31 | 数据库 | 做有积累的事情 |

| javabin | 2022.02.03 | hacker | 好 cool 的人 |

| soulteary | 2022.02.07 | DIY 神 | 向往 |

| 7dot9 | 2022.02.26 | 前辈 | 梦在这里可以飞翔 |

| chrisdown | 2022.03.05 | SRE | Linux |

]]>
我平时一直都在发现很多优质的资源,但都没有整理。一直增加,单独一个页面可能开始会有些乱,就先开一篇放在这里。
Nginx 代理静态网站 CSS 解析异常 nginx-website 2023-01-31T21:55:01+08:00 2023-01-31T21:55:01+08:00

今天在使用ecs进行部署网页时,出现了一个问题。使用nginx代理到页面index.html路径下,同路径的资源都可以加载到,但是却无法正确加载到页面样式。打开f12,网络和控制台都没有资源异常,但页面乱成了一锅粥。

本地打开是正常的,上到服务器却不行?

之前使用nginx时,并没有这个问题,于是我猜测是不是nginx新的版本对配置参数进行了修改?

但我翻看了nginx的文档,却没有找到。于是我跟着症状开始在网上翻文,终于:

解决方法

若不对于css文件解析进行配置,nginx默认文件都是text/plain类型进行解析,为此我们需要对此进行简单配置。将下面这段代码放入到location模块下面,然后重启nginx。记住一定要清理浏览器的缓存


include mime.types;  

default_type application/octet-stream;

完整如下:


location / {

              include mime.types;

              default_type application/octet-stream;

              alias /opt/website/;

              autoindex on;

         }



]]>
今天在使用ecs进行部署网页时,出现了一个问题。使用nginx代理到页面index.html路径下,同路径的资源都可以加载到,但是却无法正确加载到页面样式。打开f12,网络和控制台都没有资源异常,但页面乱成了一锅粥。 本地打开是正常的,上到服务器却不行?
macOS 12 Monterey + Windows11 我的生产力答案 hackintosh 2022-12-25T21:14:27+08:00 2022-12-25T21:14:27+08:00

大学毕业后,我买了一台乞丐版的 13 寸 MacBook Pro,这台电脑配置比现在的安卓手机还低。今年年中,我开始把用于工作的 MacBook Pro(18 款 15 寸 16+256)带回家使用,虽然勉强能用,但是我渐渐地发现这两台电脑都不能满足我的需求。

从 2020 年开始,我就计划安装一台黑苹果,我把很多零件加入购物车,但是当时显卡实在太贵,所以一直没有开始。后来以太坊价格直线上涨,矿卡价格也水涨船高,苹果公司也开始全面转向 ARM 平台,这使我更加渴望拥有一台强大的“生产力”工具,赶上黑苹果的尾巴。

在我看来,相比苹果设备,黑苹果有以下几个优点:

  1. 价格便宜。

  2. 可以自定义硬件配置,不需要精准的安装技能。

  3. 可以安装 x86 平台的 Windows 系统,可以用于娱乐和生产力。

  4. 探索的过程也是一种快乐。

价格方面,只需要 1000 元左右就可以购买 32GB DDR5 内存,500多块可以买到2TB 固态硬盘等等。


自定义硬件方面,我可以先买一个 32GB 的内存条,然后等到有钱了再买一个(众所都周知,苹果的内存条是金子做的,真买不起)。

同样的道理,我也可以先买一个 5600XT 显卡(甚至是矿卡),然后再换成 6600XT 等更强的显卡。


安装 Windows 是很有必要的,因为无论是娱乐还是工作,Windows 都是必不可少的。

如果你觉得双系统切换麻烦,这个工具了解一下:fa-hand-o-down:

image.png


几个劣势:

  1. 体积大

  2. 兼容性,稳定性差。

  3. 可能无法升级。

  4. 功耗大。

  5. 需要花费一些时间。

在体积上,再怎么取舍,也会远远超过mac stdio的完美集成。我使用的机箱是联立A4 H2O,是比较经典的A4机箱,也要11L的空间。但我这里可以装下一个240水冷,更大的体积也会带来更好的散热。

兼容性的话,在装机时候,需要很多的设置,扒帖,去定制更适配的EFI。

无法升级这个可能存在的问题,我尽量选择新的系统中的稳定的主流的系统,这样,在一次好好准备之后,接近完美的一个系统可以用很久。

功耗,对一个普通台式机玩家,不是问题。

花费时间是可以取舍的,如果想要尽量快,就去尽量找别人准备好的,可能会花费几块钱;如果喜欢折腾,就去扒帖,学习设置。白果只有物流时间。


然后,我进行了一些性能测试。总体来说,我的黑苹果系统表现不错,性能比我之前的MacBook Pro要好得多。我可以运行Adobe Creative Suite和Final Cut Pro等专业软件,没有出现任何问题。

此外,我也可以在我的黑苹果上安装Windows系统,这样我就可以玩一些Windows平台上的游戏了。这是Mac OS系统所不能提供的,这也是我决定装黑苹果的原因之一。

当然,黑苹果也有一些劣势,比如兼容性和稳定性相对较差,可能需要花费一些时间来解决问题。但是,对于我这种喜欢折腾的人来说,这些问题并不是什么大问题。我可以通过网上的资源和社区来解决这些问题。

总的来说,装黑苹果是一项有趣的尝试。虽然过程可能会有些繁琐,但是如果你是一个喜欢尝试新事物和折腾的人,那么我认为你可以尝试一下。:tw-1f603:


然后就是我的整个装机历程了:

零件:


1. 机箱 联立A4 H2O 900RMB

2. 电源 长城tf750 729RMB

3. 主板 ROG B660itx 1250RMB

4. 内存 海盗船DDR5 5200MHZ 1000RMB

5. CPU i512490F盒装 1050RMB

6. 显卡 蓝宝石5600XT 白金版 6G 628RMB

7. 固态 七彩虹2TB 569RMB

8. 水冷 利民冰封幻境240 RGB 300RMB

共: 6426RMB

win11固态出厂预装

在网上找到B660I 主板设置好的EFI文件

下载和这个EFI文件适配的带引导的dmg系统镜像,直接写盘,更换EFI,重启安装。

之后我会把相对应的教程和文件贴一下,如果有同学看到了,感兴趣,着急的可以直接留言,我会尽快整理的。 :tw-1f618:

]]>
大学毕业以后,买了一台乞丐版的13寸的macbook pro(8 + 128 这个配置可能还不如现在的安卓手机),今年年中,我开始把工作用的macbook pro(18款 15寸 16 + 256)往家里带,勉强够用。到了今年下半年,我渐渐感觉,这俩电脑,一个能打的都没有,真顶不住了。
最新版 Let’s Encrypt 免费证书申请步骤,保姆级教程 lets-encrypt 2022-12-02T19:17:50+08:00 2022-12-02T19:17:50+08:00

最近将域名迁到了google domain,就研究了一下Let’s Encrypt的域名证书配置。发现网上找到的教程在官方说明中已经废弃,所以自己写一个流程记录一下。

步骤方法官方文档见:https://eff-certbot.readthedocs.io/en/stable/install.html#installation

snapd官方文档见:https://certbot.eff.org/instructions

1. 安装snapd(这里我使用的是centos系统)


   sudo yum install snapd

   sudo systemctl enable --now snapd.socket

   sudo ln -s /var/lib/snapd/snap /snap

2. 使用snapd安装certbot


   sudo snap install --classic certbot

   sudo ln -s /snap/bin/certbot /usr/bin/certbot

####3. 生成证书(需要指定nginx)

:fa-chevron-circle-right: 手动安装nginx


   certbot certonly --nginx --nginx-ctl /usr/local/nginx/sbin/nginx --nginx-server-root /usr/local/nginx/conf

这里的certonly就是只下载对应文件,不进行配置nginx,适用于自己配置或者更新使用。去掉则会帮你进行配置nginx(我没有试用)。

可能出现的问题:

The error was: PluginError(‘Nginx build is missing SSL module (–with-http_ssl_module).’)

提示这个错误是因为目前nginx缺少–with-http_ssl_module这个模块,我们要添加这个模块。重新编译nginx

进入nginx下载的目录

./configure –prefix=/usr/local/nginx –with-http_ssl_module

编译完成后

make

make install

/usr/local/nginx/sbin/nginx -s stop

/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf

使用 /usr/local/nginx/sbin/nginx -V 查看是否生效


   [root]# /usr/local/nginx/sbin/nginx -V

   nginx version: nginx/1.23.2

   built by gcc 10.2.1 20200825 (Alibaba 10.2.1-3 2.32) (GCC) 

   built with OpenSSL 1.1.1k  FIPS 25 Mar 2021

   TLS SNI support enabled

   configure arguments: --prefix=/usr/local/nginx --with-http_ssl_module

生效,重新执行上面的命令即可。

4. 生成证书

执行上面的命令后,程序会让你确认你的邮箱和你的域名,确认完成后会将证书文件生成在指定目录中。


   certbot certonly --nginx --nginx-ctl /usr/local/nginx/sbin/nginx --nginx-server-root /usr/local/nginx/conf

   Saving debug log to /var/log/letsencrypt/letsencrypt.log

   Enter email address (used for urgent renewal and security notices)

    (Enter 'c' to cancel): [这里输入你的邮箱]

   

   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

   Please read the Terms of Service at

   https://letsencrypt.org/documents/LE-SA-v1.3-September-21-2022.pdf. You must

   agree in order to register with the ACME server. Do you agree?

   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

   (Y)es/(N)o: Y

   

   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

   Would you be willing, once your first certificate is successfully issued, to

   share your email address with the Electronic Frontier Foundation, a founding

   partner of the Let's Encrypt project and the non-profit organization that

   develops Certbot? We'd like to send you email about our work encrypting the web,

   EFF news, campaigns, and ways to support digital freedom.

   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

   (Y)es/(N)o: Y [选Y 继续]

   Account registered.

   

   Which names would you like to activate HTTPS for?

   We recommend selecting either all domains, or all domains in a VirtualHost/server block.

   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

   1: whrss.com

   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

   Select the appropriate numbers separated by commas and/or spaces, or leave input

   blank to select all options shown (Enter 'c' to cancel):  [这里不需要输入,回车选所有]

   Requesting a certificate for whrss.com

   

   Successfully received certificate.

   Certificate is saved at: 

   # [这里告诉我们生成的文件路径和有效期]

   /etc/letsencrypt/live/whrss.com/fullchain.pem

   Key is saved at:         /etc/letsencrypt/live/whrss.com/privkey.pem

   This certificate expires on 2023-03-02.

   These files will be updated when the certificate renews.

   Certbot has set up a scheduled task to automatically renew this certificate in the background.

   

   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

   If you like Certbot, please consider supporting our work by:

    * Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate

    * Donating to EFF:                    https://eff.org/donate-le

   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

   

5. Nginx.conf的配置


   server{

           #监听443端口

           listen 443 ssl;

           #对应的域名,空格分隔域名就可以了

           server_name whrss.com; 

           #第一个域名的文件

           ssl_certificate /etc/letsencrypt/live/whrss.com/fullchain.pem;

           ssl_certificate_key /etc/letsencrypt/live/whrss.com/privkey.pem;

           # 其他配置

           ssl_session_timeout 5m;

           ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

           ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;

           ssl_prefer_server_ciphers on;

           #这是我的主页访问地址,因为使用的是静态的html网页,所以直接使用location就可以完成了。

           location / {

               root   /;

               index  /;

               proxy_pass http://127.0.0.1:9091;

               proxy_set_header Host $host:443;

               proxy_set_header X-Real-IP $remote_addr;

               proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

               proxy_set_header Via "nginx";

           }

   }

以上!!

]]>
最近将域名迁到了google domain,就研究了一下Let’s Encrypt的域名证书配置。发现网上找到的教程在官方说明中已经废弃,所以自己写一个流程记录一下。
五人墓碑记 tombstone-of-the-five 2022-11-28T10:02:25+08:00 2022-11-28T10:02:25+08:00
作者:张溥 明

五人者,盖当蓼洲周公之被逮,激于义而死焉者也。至于今,郡之贤士大夫请于当道,即除逆阉废祠之址以葬之,且立石于其墓之门,以旌其所为。呜呼,亦盛矣哉!夫五人之死,去今之墓而葬焉,其为时止十有一月尔。夫十有一月之中,凡富贵之子,慷慨得志之徒,其疾病而死,死而堙没不足道者,亦已众矣,况草野之无闻者欤?独五人之皦皦,何也?

予犹记周公之被逮,在丁卯三月之望。吾社之行为士先者,为之声义,敛赀财以送其行,哭声震动天地。缇骑按剑而前,问:“谁为哀者?”众不能堪,抶而仆之。是时以大中丞抚吴者,为魏之私人,周公之逮所由使也。吴之民方痛心焉,于是乘其厉声以呵,则噪而相逐,中丞匿于溷藩以免。既而以吴民之乱请于朝,按诛五人,曰颜佩韦、杨念如、马杰、沈扬、周文元,即今之傫然在墓者也。然五人之当刑也,意气阳阳,呼中丞之名而詈之,谈笑以死。断头置城上,颜色不少变。有贤士大夫发五十金,买五人之脰而函之,卒与尸合。故今之墓中,全乎为五人也。

嗟夫!大阉之乱,缙绅而能不易其志者,四海之大,有几人欤?而五人生于编伍之间,素不闻《诗》《书》之训,激昂大义,蹈死不顾,亦曷故哉?且矫诏纷出,钩党之捕遍于天下,卒以吾郡之发愤一击,不敢复有株治。大阉亦逡巡畏义,非常之谋,难于猝发。待圣人之出而投环道路,不可谓非五人之力也。

繇是观之,则今之高爵显位,一旦抵罪,或脱身以逃,不能容于远近,而又有翦发杜门,佯狂不知所之者,其辱人贱行,视五人之死,轻重固何如哉?是以蓼洲 周公,忠义暴于朝廷,赠谥美显,荣于身后。而五人亦得以加其土封,列其姓名于大堤之上,凡四方之士,无有不过而拜且泣者,斯固百世之遇也。不然,令五人者保其首领,以老于户牖之下,则尽其天年,人皆得以隶使之,安能屈豪杰之流,扼腕墓道,发其志士之悲哉?故予与同社诸君子,哀斯墓之徒有其石也,而为之记,亦以明死生之大,匹夫之有重于社稷也。

贤士大夫者,冏卿因之吴公、太史文起文公、孟长姚公也。

]]>
五人墓碑记 明 张溥  卒以吾辈之发愤一击,不敢复有株治。
随想—生活效能 effectiveness 2022-11-27T15:34:45+08:00 2022-11-27T15:34:45+08:00

之前我有写过一篇文章,是关于边际效益的思考的,但很不幸,我没有备份,一并被阿里云ban掉了。

那篇文章是基于李永乐老师的一个视频——996的那个,来讨论的。最近,我发现在生活中,我有不少关于效益的习惯和思考,记录一下。

最近,我意识到自己对于效益的思考和行为有不少习惯,其中一个例子是洗衣服。由于我租住的房子没有洗衣机,而我个人穿着简单,衣服也很少,所以我通常都是手洗。刚开始,我只是随便洗一下,因为感觉这样能省事,但是随着时间的推移,我开始思考这个过程中的效益和成本,并且逐渐调整了我的洗衣方式。

首先,我从心理层面分析了这个问题。我从洗衣液问世时就开始使用它,因为它能轻松地溶解脏污,尤其是一些不易清洗的面料。这种想法让我认为只要用洗衣液泡一下,就能够解决70%左右的污渍。对于日常穿着,这种简单的清洗方式足以满足需求。但是对于有些衣服,比如容易掉色的棉制外裤,洗得太狠会影响寿命,这也是我使用简单方法洗衣服的原因之一。此外,冬天水太冷,而且洗衣机所需的衣服数量太多,所以我不得不简化洗衣的流程。

接着,我开始考虑“边际效益”。我想到了洗衣过程中从“干净”到“脏”的过渡,这个过程需要进行比较,比如,当衣服开始散发出汗味时,就需要清洗。我需要洗衣服来保证我的整体干净程度。在控制洗衣液和水温等变量条件下,衣服的干净程度和所需要付出的劳动基本上可以看作一个效益曲线。这个曲线上有一个最佳点,也就是衣服的干净程度和所付出的劳动量之间的比例最佳。在这个点上,我们付出的劳动和晾晒的时间等等,可以最大程度地轻松达到一个干净的程度。如果要让衣服更加干净,所需付出的劳动就会越来越大。

因此,我选择了一种简单而有效的洗衣方式:浸泡、揉搓、涮净、拧干和晾晒。这个过程几乎不需要花费多少时间和经历,在让衣服变得干净的同时,也会大大延长衣物的寿命。

效能,效就是结果,能就是取效所做的功。在实现目标的过程中,所需付出的劳动与产生的效果之间存在一种平衡点,我们需要找到这个点,以达到最大化效益的目的。

]]>
效能,效就是结果,能就是取效所做的功。
测试一下功能吧 test-function 2022-11-24T21:11:52+08:00 2022-11-24T21:11:52+08:00

阿里服务器被ban了,数据全没了,测试一下重新组织博客能否正常工作吧


全部组件都使用了国外免费的平台。

]]>
阿里服务器被ban了,数据全没了