Nginx Ingress http 请求413状态码问题及解决方法

问题 最近在调用一个上传文件的接口时,发现接口调用响应状态码为413,并且控制台显示跨域错误信息。查找了相关信息,得知 413 状态码表示请求的包体过大导致的。 出现这种情况,我想到了2种解决方案: 1) 调整上传文件的方式 2) 调整网关的参数。 综合目前的现况,采取了第二种方式调整网关客户端请求体最大值的参数。 解决 通过查阅 nginx ingress 的文档, 得知可以添加 nginx.ingress.kubernetes.io/proxy-body-size 注解来设置请求体的最大值, 设置 nginx.ingress.kubernetes.io/proxy-body-size 值为合适的值后,再请求接口发现接口顺利响应。 小结 本文介绍了客户端请求接口时,由于 nginx 默认 proxy-body-size 参数太小, 导致请求413 的问题及相应的解决方案。 参考 https://opendocs.alipay.com/support/01rb44 https://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size https://github.com/kubernetes/ingress-nginx/blob/main/docs/user-guide/nginx-configuration/annotations.md#custom-max-body-size

九月 16, 2023 · overstarry

playwright-go浏览器截图

以前的文章介绍了 chromedp 进行浏览器网页截图, 这次介绍一种新的网页截图的方法——即使用 playwright 进行浏览器网页截图。 playwright 介绍 Playwright是一个用于自动化浏览器操作的开源工具集。它由微软开发并于2020年发布,旨在提供一种跨浏览器、跨平台的解决方案,可用于测试Web应用程序、编写爬虫、执行自动化任务等。 Playwright支持多种主流浏览器,包括Chrome、Firefox、Safari和Edge,它提供了一组简单易用的API,可以模拟用户与Web页面的交互行为,例如点击、填写表单、导航等。与其他类似工具相比,Playwright的一个重要特点是它的跨浏览器支持,这意味着你可以使用相同的代码在不同浏览器上运行你的自动化任务,而不需要为每个浏览器单独编写代码。 Playwright还提供了强大的调试功能,可以帮助开发人员在自动化过程中定位和解决问题。它支持截图和录制操作,使得调试变得更加直观和高效。 另外,Playwright还具有一些高级功能,例如可以模拟不同的设备、网络环境和地理位置,以及支持并发执行多个浏览器实例等,这些功能使得它在编写复杂的自动化任务时非常有用。 由于我日常主要使用 go 语言进行开发,所以本文的内容主要以 playwright 的 go 模块 playwright-go 为主要介绍。 安装 go get -u github.com/playwright-community/playwright-go 安装相关浏览器和操作系统依赖项: go run github.com/playwright-community/playwright-go/cmd/playwright@latest install --with-deps # Or go install github.com/playwright-community/playwright-go/cmd/playwright@latest playwright install --with-deps 也可以在代码中使用以下代码安装: err := playwright.Install() 通过安装截图可以看出安装了3大主流浏览器和ffmpeg。 例子 接下来我们看一个官方的例子,从Hacker News中抓取当前投票最高的项目。 package main import ( "fmt" "log" "github.com/playwright-community/playwright-go" ) func main() { pw, err := playwright.Run() if err != nil { log.Fatalf("could not start playwright: %v", err) } browser, err := pw.Chromium.Launch() if err != nil { log.Fatalf("could not launch browser: %v", err) } page, err := browser.NewPage() if err != nil { log.Fatalf("could not create page: %v", err) } if _, err = page.Goto("https://news.ycombinator.com"); err != nil { log.Fatalf("could not goto: %v", err) } entries, err := page.Locator(".athing").All() if err != nil { log.Fatalf("could not get entries: %v", err) } for i, entry := range entries { title, err := entry.Locator("td.title > span > a").TextContent() if err != nil { log.Fatalf("could not get text content: %v", err) } fmt.Printf("%d: %s\n", i+1, title) } if err = browser.Close(); err != nil { log.Fatalf("could not close browser: %v", err) } if err = pw.Stop(); err != nil { log.Fatalf("could not stop Playwright: %v", err) } } ...

九月 2, 2023 · overstarry

使用 Google Api Go Client 调用 Google Adsense 报告接口失败的问题及解决方案

问题 最近需要使用 Google Api Go Client 获取 Google Adsense的数据,由于常规 generate 接口返回的数据是有10w行的限制,不满足目前的使用需要,于是使用了 csv的生成方法 generateCsv,和 generate 相比 generateCsv 能返回更多的数据并且不需要改动太多参数。 在调用 generateCsv 接口时,发现接口不能返回正确数据,提示错误,错误信息如下: panic: invalid character 'D' looking for beginning of value 根据错误信息可以得知错误应该是解析错误,通过方法接口和源代码看出,接口返回的是csv类型的数据,但 api 只针对 json 进行解析,没有判断数据类型,导致解析失败。 解决 找到问题后,我立即就想到一个方法,就是在decode代码里添加csv格式的判断,尝试了一下,由于对csv解析方法不熟悉,并且修改 Client 代码会导致模块更新不方便(客户端代码都是生成的,于是决定另寻它法。 通过寻找各种资料issue后,找到了一个可能可行的方法,就是创建手动创建http连接发送请求。 根据相应的文档,很快就写好了相应的代码: package main import ( "context" "fmt" "golang.org/x/oauth2" "golang.org/x/oauth2/google" "google.golang.org/api/adsense/v2" "google.golang.org/api/option" "google.golang.org/api/transport/http" "io" "net/url" ) func main() { ctx := context.Background() var config = &oauth2.Config{ ClientID: "xxx.apps.googleusercontent.com", // from https://console.developers.google.com/project/<your-project-id>/apiui/credential ClientSecret: "GOCSPX-xxxxxx", // from https://console.developers.google.com/project/<your-project-id>/apiui/credential Endpoint: google.Endpoint, Scopes: []string{adsense.AdsenseReadonlyScope}, RedirectURL: "https://developers.google.com", } //token := newOAuthClient(ctx, config) client, _, err := http.NewClient(ctx, option.WithTokenSource(config.TokenSource(ctx, &oauth2.Token{RefreshToken: "code"}))) if err != nil { return } URL, err := url.Parse(fmt.Sprintf("https://adsense.googleapis.com/v2/accounts/pub-123123/reports:generateCsv")) if err != nil { return } query := URL.Query() query.Set("dateRange", "YESTERDAY") query.Set("dimensions", "URL_CHANNEL_NAME") query.Set("metrics", "PAGE_VIEWS") URL.RawQuery = query.Encode() resp, err := client.Get(URL.String()) if err != nil { return } defer resp.Body.Close() all, err := io.ReadAll(resp.Body) if err != nil { return } fmt.Println(string(all)) } 主要逻辑就是创建到google cloud service 的连接,调用 generateCsv 接口即可。 ...

八月 26, 2023 · overstarry

gonew介绍

前几周。在关注的go提案每周会议中,发现了一条比较感兴趣的内容: gonew: templates for new modules ,通过标题可以猜到 gonew 应该是一个通过配置项目模板生成新项目的模块。 通过对 discussions 中 rsc 所描述的内容进行分析,可以得知为什么要启动这么一个新项目: go团队经常收到用户的请求,想要通过模板启动一个新项目,即以某种基本的项目模板来创建一个新Go module。Russ私下编写了一个实现这个功能的小工具:rsc.io/tmp/gonew. Russ在google内部宣传该工具后,Google内部的一些团队便定制了一些模板(template) ,尤其是ServiceWeaver团队的响应尤为积极。这一切最终让Russ决定引入golang.org/x/tools/cmd/gonew。 gonew工具的引入大幅简化了Go项目的创建,同时由于对自定义项目模板的支持,也可以提高Go项目的标准化水平。目前 gonew 工具是实验性的,后续可能会增加新的特性,但目前的核心功能是会保留的。 通过对 discussions 中社区用户开发着的回应可以看出,大家纷纷讲述了没有 gonew 前所使用的工具,并对 gonew 建言献策,可以看出大部分的开发者都十分欢迎这个新功能的。 接下来就由我来介绍 gonew 。 安装 通过以下命令安装 gonew: go install golang.org/x/tools/cmd/gonew@latest $ go install golang.org/x/tools/cmd/gonew@latest go: downloading golang.org/x/tools v0.12.0 go: downloading golang.org/x/mod v0.12.0 执行 gonew: $ gonew usage: gonew srcmod[@version] [dstmod [dir]] See https://pkg.go.dev/golang.org/x/tools/cmd/gonew. 接下来我来介绍 gonew 的几种用法。 使用 用法1 用法1是基于模板创建同名项目,我们以 https://github.com/minio/mc 这个项目为例子。 执行 gonew github.com/minio/mc ...

八月 19, 2023 · overstarry

ent相同列名排序问题解决

前言 最近在开发的时候,需要进行数据库计算,主要是根据表中的某些字段进行汇总计算,但由于数据库表中已有同名字段名,ent 不会使用计算后的指标,默认使用schema中定义的字段,导致无法返回正确的结果。 针对这种情况,我能想到的方法有2种: 1) 不使用同名的字段名 2) 查找ent是否有相关的解决方案。 这里我采用了第二种方法,查找相关的issues, 通过查找相关issue,找到了相关的解决方案: ent 的 sql/modifier 特性。 场景重现 定义一个新的数据库表结构,结构如下: func (Ad) Fields() []ent.Field { return []ent.Field{ field.Float("estimated_earnings"), field.Int("page_views"), field.Time("date"), field.Float("page_views_rpm").Optional(), } } page_views_rpm 字段是由 estimated_earnings 和 page_views 计算而来。 编写相应的查询代码: package main import ( "context" "entgo.io/ent/dialect/sql" "fmt" _ "github.com/lib/pq" "log" "modifier-demo/ent" "modifier-demo/ent/ad" "time" ) type Ads struct { EstimatedEarnings float64 `json:"estimated_earnings"` PageViews int64 `json:"page_views"` Date time.Time `json:"date"` PageViewsRpm float64 `json:"page_views_rpm"` } func main() { client, err := ent.Open("postgres", "host=127.0.0.1 port=5432 sslmode=disable user=postgres dbname=data_test password=mysecretpassword") if err != nil { log.Fatalf("failed opening connection to postgres: %v", err) } defer client.Close() // Run the auto migration tool. if err := client.Schema.Create(context.Background()); err != nil { log.Fatalf("failed creating schema resources: %v", err) } var a []Ads err = client.Debug().Ad.Query().Order(ent.Desc(ad.FieldPageViewsRpm)).GroupBy(ad.FieldDate).Aggregate(func(selector *sql.Selector) string { return sql.As(" CAST(COALESCE(SUM(estimated_earnings) / NULLIF(SUM(page_views)*1.0, 0.0)*1000, 0)AS numeric(10,2))", "page_views_rpm") }).Scan(context.TODO(), &a) if err != nil { return } fmt.Println(a) } 主要是根据date来汇总并重新计算 page_views_rpm 字段,运行代码后发现没有成功输出,打印后发现ent使用了旧的PageViewsRpm字段进行排序,导致sql无法顺利运行。 ...

八月 12, 2023 · overstarry

Golang_embed简单介绍

最近需要使用 golang1.16 中的功能 embed ,本文简单记录下 embed 的使用。 embed 介绍 Go 1.16 引入了embed包,允许我们在编译时将静态文件(例如 .go、.html、.css、.js 等)嵌入到 Go 源文件中。这在构建静态网站、单页应用程序(SPA)和其他项目时非常有用。 主要有几个优点: 方便部署:不需要再部署静态资源文件,所有的资源都直接嵌入到可执行文件中。 安全:用户无法直接访问或修改嵌入的文件。 版本管理:和Go代码一起版本控制。 使用 嵌入为字符串 可以将文件内容保存到字符串变量中。 package main import ( _ "embed" "fmt" ) //go:embed hello.txt var s string func main() { fmt.Println(s) } 文件路径下有个 hello.txt,内容如下:hello, overstarry,代码运行输出:hello, overstarry 保存为 []bytes 还可以将文件内容保存为 []bytes变量 package main import ( _ "embed" "fmt" ) //go:embed hello.txt var s []byte func main() { fmt.Println(string(s)) } 运行代码输出内容与上面一致。 保存为 fs.FS 类型 还可以将文件保存为 fs.FS 类型,这在嵌入多个文件时非常有用(string和[]bytes不支持多个//go:embed指令)。 ...

八月 5, 2023 · overstarry

Nginx反向代理中出现的问题及解决方法

最近在使用nginx反向代理时遇到了一些问题,在本文记录一下问题及相应的解决方法。 问题1 问题现象 错误日志如下: : host not found in upstream "xx.xx.vip"in /etc/nginx/conf.d/default.conf:17 nginx: [emerg] host not found in upstream "xx.xx.vip”in /etc/nginx/conf.d/default. conf:17 从错误日志可以看出这个问题主要是nginx无法解析相应的域名。 解决方法 怎么解决这个问题呢,我们只需添加相应的dns服务器即可 resolver 8.8.8.8; 。 问题2 问题现象 错误日志如下: 2023/07/28 01:35:43 [error] 34#34: *44 SSL_do_handshake() failed (SSL: error:14094438:SSL routines:ssl3_read_bytes:tlsv1 alert internal error:SSL alert number 80) while SSL handshaking to upstream, client: 172.16.64.75, server: , request: "POST /v1/data/xx/filter HTTP/1.1", upstream: "https://xxxx:443/v1/data/xx/filter", host: "xx.xx.com", referrer: "https://xx.xx.com/user/login" 解决方法 这个问题主要是https相关配置的问题,我们只需添加这几行配置即可: proxy_ssl_session_reuse off; proxy_ssl_server_name on; proxy_ssl_protocols TLSv1 TLSv1.1 TLSv1.2; proxy_ssl_session_reuse off; 关闭默认尝试重新使用SSL上游的SSL会话 proxy_ssl_server_name on; 反向代理的时候,通过域名而不是ip地址去访问 proxy_ssl_protocols 指定SSL协议 ...

七月 28, 2023 · overstarry

使用cert Manager申请免费证书

cert manager 介绍 cert-manager 是一个可信证书管理器,可以自动为您的集群中的服务提供SSL证书和可靠的基础设施。它执行以下任务: 自动通过类似 Let’s Encrypt 和 eigene 之类的 Certificate Authority (CA) 重新生成即将过期的证书。 为您的集群中的服务自动生成证书。 提供一个组件库,可用于自签名证书或其他CA API。 主要特性: 自动生成/重新生成证书 支持多种 CA 颁发的证书:Let’s Encrypt、自签名证书、HashiCorp Vault 等 支持多种记录类型:DNS01、HTTP01、Kubernetes Ingress 等 适用于支持 TLS 密码学的任何 Kubernetes API 对象 具有密钥轮换功能,可无缝替换即将到期的证书 流畅的 API,易于扩展 总的来说,cert-manager 可以让您将集群负载均衡的 TLS 实现自动化,减少运维负担。它主要用于解决基础设施中最常见的挑战:如何高可用地为应用提供 TLS 证书。 申请证书 接下来我来讲解本文的重点:如何使用 cert-manager 申请证书并配置于ingress上。 安装 cert-manager 执行 kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml 部署最新版本 cert-manager. 查看 pod状态 部署后,过一会执行 kubectl get pods -n cert-manager 查看pod状态。 ...

七月 22, 2023 · overstarry

Golang i18n

最近在开发一个需求时,需要将英文转为中文,这就需要进行本地化的处理,通过查找相关的库,决定使用 gettext-go 来进行本地化的处理,本篇文章主要简单介绍 gettext-go 和 它在k8s kubectl 中的运用。 gettext-go 简单介绍和使用 gettext-go 简单来说就是读取预先编写的 po或mo文件来进行本地化的处理. po和mo文件是什么呢? 接下来介绍一下po和mo文件 po和mo文件介绍 .po文件,.mo文件是由gettext程序生成或者使用的源代码和编译结果。 1、.pot文件 是一种模板文件,其实质与.po文件一样,其中包含了从源代码中提取所有的翻译字符串的列表,主要提供给翻译人员使用。 2、.po文件 (1)用程序msginit来分析pot文件,生成各语言对应的po文件,比如中文就是zh_CN.po,法语就是fr.po文件。 (2)PO是Portable Object(可移植对象)的缩写形式,它是面向翻译人员的、提取于源代码的一种资源文件。 (3).po文件可以用任何编辑器如poEdit,vi,Emacs,editplus打开,交给翻译人员来将其中的文字翻译成本国语言。 3、.mo文件 (1)用msgfmt将.po文件编译成mo文件,这是一个二进制文件,不能直接编辑。 (2)MO是Machine Object(机器对象)的缩写形式,它是面向计算机的、由.po文件通过GNU gettext工具包编译而成的二进制文件,应用程序通过读取.mo文件使自身的界面转换成用户使用的语言,如简体中文。 (3)可以用工具如msgunfmt命令将.mo文件反编译为.po文件。 很多软件都是通过这些文件实现多语言的功能。 gettext-go 简单使用 package main import ( "fmt" "github.com/chai2010/gettext-go" ) func main() { gettext := gettext.New("hello", "./examples/locale").SetLanguage("zh_CN") fmt.Println(gettext.Gettext("Hello, world!")) // Output: 你好, 世界! } 这段代码主要就是读取预先定义的 po或mo文件,选择中文翻译,将 Hello, world! 转为中文。这就是这个库的主要功能,接下来我们来看看这个库在k8s中使用的例子。 k8s 中的使用 k8s 中使用gettext-go 的地方是在kubectl中,主要是命令行的本地化。先简单看一下代码。 // https://raw.githubusercontent.com/kubernetes/kubernetes/master/staging/src/k8s.io/kubectl/pkg/util/i18n/i18n.go /* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package i18n import ( "archive/zip" "bytes" "embed" "errors" "fmt" "os" "strings" "sync" "github.com/chai2010/gettext-go" "k8s.io/klog/v2" ) //go:embed translations var translations embed.FS var knownTranslations = map[string][]string{ "kubectl": { "default", "en_US", "fr_FR", "zh_CN", "ja_JP", "zh_TW", "it_IT", "de_DE", "ko_KR", "pt_BR", }, // only used for unit tests. "test": { "default", "en_US", }, } var ( lazyLoadTranslationsOnce sync.Once LoadTranslationsFunc = func() error { return LoadTranslations("kubectl", nil) } translationsLoaded bool ) // SetLoadTranslationsFunc sets the function called to lazy load translations. // It must be called in an init() func that occurs BEFORE any i18n.T() calls are made by any package. You can // accomplish this by creating a separate package containing your init() func, and then importing that package BEFORE // any other packages that call i18n.T(). // // Example Usage: // // package myi18n // // import "k8s.io/kubectl/pkg/util/i18n" // // func init() { // if err := i18n.SetLoadTranslationsFunc(loadCustomTranslations); err != nil { // panic(err) // } // } // // func loadCustomTranslations() error { // // Load your custom translations here... // } // // And then in your main or root command package, import your custom package like this: // // import ( // // Other imports that don't need i18n... // _ "example.com/myapp/myi18n" // // Other imports that do need i18n... // ) func SetLoadTranslationsFunc(f func() error) error { if translationsLoaded { return errors.New("translations have already been loaded") } LoadTranslationsFunc = func() error { if err := f(); err != nil { return err } translationsLoaded = true return nil } return nil } func loadSystemLanguage() string { // Implements the following locale priority order: LC_ALL, LC_MESSAGES, LANG // Similarly to: https://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html langStr := os.Getenv("LC_ALL") if langStr == "" { langStr = os.Getenv("LC_MESSAGES") } if langStr == "" { langStr = os.Getenv("LANG") } if langStr == "" { klog.V(3).Infof("Couldn't find the LC_ALL, LC_MESSAGES or LANG environment variables, defaulting to en_US") return "default" } pieces := strings.Split(langStr, ".") if len(pieces) != 2 { klog.V(3).Infof("Unexpected system language (%s), defaulting to en_US", langStr) return "default" } return pieces[0] } func findLanguage(root string, getLanguageFn func() string) string { langStr := getLanguageFn() translations := knownTranslations[root] for ix := range translations { if translations[ix] == langStr { return langStr } } klog.V(3).Infof("Couldn't find translations for %s, using default", langStr) return "default" } // LoadTranslations loads translation files. getLanguageFn should return a language // string (e.g. 'en-US'). If getLanguageFn is nil, then the loadSystemLanguage function // is used, which uses the 'LANG' environment variable. func LoadTranslations(root string, getLanguageFn func() string) error { if getLanguageFn == nil { getLanguageFn = loadSystemLanguage } langStr := findLanguage(root, getLanguageFn) translationFiles := []string{ fmt.Sprintf("%s/%s/LC_MESSAGES/k8s.po", root, langStr), fmt.Sprintf("%s/%s/LC_MESSAGES/k8s.mo", root, langStr), } klog.V(3).Infof("Setting language to %s", langStr) // TODO: list the directory and load all files. buf := new(bytes.Buffer) w := zip.NewWriter(buf) // Make sure to check the error on Close. for _, file := range translationFiles { filename := "translations/" + file f, err := w.Create(file) if err != nil { return err } data, err := translations.ReadFile(filename) if err != nil { return err } if _, err := f.Write(data); err != nil { return nil } } if err := w.Close(); err != nil { return err } gettext.BindLocale(gettext.New("k8s", root+".zip", buf.Bytes())) gettext.SetDomain("k8s") gettext.SetLanguage(langStr) translationsLoaded = true return nil } func lazyLoadTranslations() { lazyLoadTranslationsOnce.Do(func() { if translationsLoaded { return } if err := LoadTranslationsFunc(); err != nil { klog.Warning("Failed to load translations") } }) } // T translates a string, possibly substituting arguments into it along // the way. If len(args) is > 0, args1 is assumed to be the plural value // and plural translation is used. func T(defaultValue string, args ...int) string { lazyLoadTranslations() if len(args) == 0 { return gettext.PGettext("", defaultValue) } return fmt.Sprintf(gettext.PNGettext("", defaultValue, defaultValue+".plural", args[0]), args[0]) } // Errorf produces an error with a translated error string. // Substitution is performed via the `T` function above, following // the same rules. func Errorf(defaultValue string, args ...int) error { return errors.New(T(defaultValue, args...)) } 简单看一下代码可以看出,本地化资源文件是通过 go1.16新特性 embed 方式嵌入到了 translations 变量中。主要看几个外部方法: ...

七月 16, 2023 · overstarry

rueidis 简介

简介 rueidis 是一个快速的Golang Redis客户端,支持客户端缓存、Auto Pipelining、泛型OM、RedisJSON、RedisBloom、RediSearch等功能。 Features Auto pipelining for non-blocking redis commands RESP3 中的客户端缓存 Pub/Sub, Sharded Pub/Sub, Streams Redis Cluster, Sentinel, RedisJSON, RedisBloom, RediSearch, RedisTimeseries, 等. 具有客户端缓存和乐观锁定的通用对象映射 具有客户端缓存的分布式锁 rueidis mock OpenTelemetry 集成 Hooks and other 集成 提供类似 Go-redis API 的适配器 需要注意的是由于使用了一些go1.20版本才有的特性,如果想要使用低版本go,必须安装相应的版本。 简单使用 package main import ( "context" "fmt" "github.com/redis/rueidis" ) func main() { // 创建redis客户端连接 client, err := rueidis.NewClient(rueidis.ClientOption{InitAddress: []string{"127.0.0.1:6379"}}) if err != nil { panic(err) } defer client.Close() ctx := context.Background() // 执行 redis set 命令 err = client.Do(ctx, client.B().Set().Key("key1").Value("val").Nx().Build()).Error() if err != nil { panic(err) } hm, err := client.Do(ctx, client.B().Get().Key("key1").Build()).ToString() if err != nil { panic(err) } fmt.Println(hm) } go-redis适配器 如何快速从 go-redis 切换到 rueidis 客户端呢,rueidis 提供了rueidiscompat.NewAdapter 方法,通过Adapter可以使用熟悉的go-redis中的方法。 ...

七月 8, 2023 · overstarry