# 镜湖
> 镜湖 - 忘机山人的个人博客,专注 AWS 云计算、Docker 容器、NAS 与懒猫微服、Easysearch、Python 后端开发、AI 部署与 Homelab 实践,记录技术、生活与思考
本文件包含全站已发布文章的正文纯文本,按发布日期倒序排列,面向需要完整上下文的大语言模型与 AI 搜索引擎。
索引版本见 llms.txt。
- 站点: https://blog.no-claw.com/
- 作者: 忘机山人
- 语言: zh-CN
- 文章数: 313
---
## 五种时间摘抄
- URL: https://blog.no-claw.com/posts/c0512844/
- 日期: 2026-05-06
- 分类: 摘抄
只有当人们不再被旧的框架束缚,为自己创造一个天然和自由的环境,才有能力把这个时代巨大的信息冲刷变成养料、阳光和水分,用它们来滋养生活。当你能够对信息进行重新辨别和分类,把旧有、无效的待办事项杀死在脑中,就可以使用这种能力做到你从未做到的事情。到那时,你能够引导和控制无边无际的闪念,并利用它们来实现你的愿景。
成长中最大的成本是战略成本和时间成本,要分清什么是真正的节约,什么是真正的浪费。如果是真正想做的事,就要充满耐心。
不知道从哪里开始,不明白一切是为了什么;
梦想时有时无,目标模糊不清;
间歇性动力丧失,难以持续专注;
虚度大量碎片时间,习惯性拖延;
长期自我怀疑,纠结人生方向;
莫名恐慌焦虑,周期性情绪化;
对恋爱的未来没有期待;
反复开始,最终放弃健身;
长期分不清是懒还是累。
我们太容易被人影响了,但这或许不是我们的错,大概是这个时代独有的智能手机的错,因为我们总是主动和被动地在各种社交媒体上看到别人的生活和工作。看到带来比较,比较带来羡慕和嫉妒,这时候人就没法平静了。
如果说时间可以解决一切问题,那么当下问题的答案会写在未来。让遥远未来的自己审视今天的决定,才能分辨出重要性和急迫感,那些纵然时光倒流还是会做的事情,就是今天需要的答案。解决今天的问题,需要用未来的视角,因为那看似用不完的明天,总有一天会用完的。
一旦你看见了想要的东西,就无法再假装今天和昨天一样。
生存时间
回顾我们所知的故事,所有的胜利都因为曾经不够强大,来自缺乏和不满足。只有万事俱备时才能获得的成功,也没有资格叫作成功。所有挑战达成的基础都是资源的缺乏,当初的挑战者正处在与我们同样的生存时间之中。
生存时间的可怕之处还在于,假如你不去逾越它,它就会反过来占据、消耗和毁坏你的生活。但我们不必怕它,只要我们未来还有时间。毕竟时间是个用来计算变化的单位,时间不是用来等待的,而是用来迭代的。
“五种时间”体系对生存时间的定义是:大多数人在人生的大部分时间里,因为自身能力不足和外部条件的限制,无法做到主动选择而被动处于的时间分布。
“我的人生不会就这样了吧”是我在生存时间中会反复问自己的一句话。我的内心充满了恐惧,担心自己会被永久卡在这里,无法逃出生天。
当我在十年间循环经历了至少三轮的生存时间之后,我知道了两件事。第一,时间的确是个用来计算变化的单位,只要持续采取行动,不存在被卡住的情况,改变一定会发生;第二,即使逃出了生存时间,也是暂时的,还会进入新的生存时间去忍受煎熬。舒适永远相对的,而困难永远是绝对的。逃出,意味着去往更高的地方经受考验,题永远越做越难。
回首往昔,生存时间简直连绵不绝,我一直都忙于埋头过完自己的坎,相似的感受重复袭来,一劳永逸从未出现。
五种时间的一个好处,就是可以对当下所在的时间快速辨认和分类,从而弄清楚随之出现的情绪是怎么回事。比如生存时间当中总是充满负面情绪,当下一次你的负面情绪袭来时,如果你用最短的时间识别了它,就可以做出一些应对。当然你只能辨认情绪,并不能摆脱它,真的摆脱了情绪,就出离了过程。没有了痛苦,你就无法创造和改变。
被动生存是“能力不足,任人安排”,主动生存则是“主动争取,直面挑战”,二者同属生存时间,但有本质上的不同。你的心境会有明显的区别,处在主动生存时间之中的你,在受折磨之余其实十分清楚,面前这个更难过的关卡、更难打的怪兽,就是你曾经盼望的未来。当我觉知自己正处于生存时间中时,首要任务就是从被动生存走向主动生存。
生存时间就是这样的一种时间分布存在——占据了人生最大的比重,也不会永久消失,只会被阶段性地跨越,然后再以其他面貌重新出现,生存时间的痛苦就是人生的底色,让人对比出其他时间的快乐。
大多数人在人生的大部分时间里,因为自身能力不足和外部条件的限制,无法做到主动选择而被动处于的时间分布。我们的目标是,逾越一个又一个生存时间。
在被动生存时间中,人的情绪是压抑的,思路是迷茫的,因为大块时间被捆绑,一天中整体效率低下,被大脑边缘系统操控产生的这种负面情绪非常正常。当你读到这里,你便了解到生存时间是个必然存在。在下一次感到情绪很差,感到压抑和被动时,你要理解并知道,你现在处于生存时间之中。你会知道人在这段时间里可能就是会不快乐,但你应该马上告诉自己,眼前这个阶段不是永久的,可以解决也可以逾越,接下来不是沉浸在压抑中,而是展开让自己逾越这个阶段的行动。觉知情绪是控制情绪的前提,一旦产生觉知,就可以解决生存时间带来的大部分情绪问题。生存时间就是逆境本身,没人可以绕行。识别逆境,稳定情绪,缩短被动生存时间,开启主动生存时间。这大概就是应对生存时间的二十四字方针。
缩短被动生存时间 - 清单法”在被动生存时间中的应用
减轻焦虑
在他律中的人往往是焦虑的,尤其当你还抱有很多对自由时光的奢望时,他律中不得不做完的事务和符合你自己意愿的事务开始混淆不清。为了有效帮助自己,你需要把被动生存时间中的事务归纳在同一个清单中。为了让自己不压抑,情绪稳定,获得自由,你现在应该只关注这个清单的待办事项,目的只有一个,尽快按要求做完,缩短其所占时间。清单在手,当你写下要做的每一件事,闪现的每一个想法,用清晰的条目一一陈列,你的头脑会由一团混沌变得无比明朗。你不再因为事情堆成一团而无力,你量化了所有的他律。
专注当下
清单可以让你在不运用宝贵工作记忆的情况下,记住大部分的任务。只要你开始使用清单,它就像一个外脑,暂时帮你存储当下不重要的信息,允许你的大脑全速运算目前正在做的这件事,你会获得更为专注的能力。而每一个列于清单的事项,就像一张航海图上的坐
标,沿着清单规划的线路,一个个坐标得以抵达,你会更有效率。
摆脱他律
“自由”这一观念在西方哲学里一直有两方面的内涵,其一是自主决策,其二就是自我节制。清单法开启一种“主动选择”的行为方式,是自律的初级表现形式。康德所说的“自由即自律”,是一种高度概括的表达。“时间管理”的终极目的,是实现自由意志。自律是去往自由的通道,通过自律可以获得更多时间与机会主动权,它的意义并不只在于自律本身。当你自己不能做到强有力地规划和把握以至于达到自由时,就会有别人规划你、占据你,要么依附于他人,要么被他人管制。
自律从来都是很令人讨厌的一个词,但当它站在他律面前时,就变得可爱起来。人的根本追求是释放天性,自己做主,自由自在,但他律是被别人管理和约束,显然背离了人性,所以让人不舒服。列清单、做计划和打钩,这些小动作看似日常,但一定是为了逃离他律的可能性在努力,是人的追求和抗争。你甚至可以认为这种行为已经具有了高级的人类精神追求和哲学层面的意义。
4.总览全局,拆解目标,设定里程碑
在被动生存时间中,你最好的选择,就是做一个毫无感情的做任务机器。如果想又好又快地做任务,清单必须成为这个机器的组成部分。当你为项目中的所有待办事项列出清单,就已经把一个大目标拆解成若干小目标,小目标就是清晰的里程碑,无论是看起来多么遥不可及、庞杂的事项,在你这里都被有序拆分完毕。纵然身处他律中又怎样呢?做咸鱼,也要做咸鱼里最咸的那条呀。
打钩的快感
把扑面而来的任务一个个消灭,做完就打钩,看着它们由多变少,统统消失,打钩会是生存时间中为数不多的一大乐趣。当你完成了清单上的一件事,无论大小,你会获得很确定的完成感,即便它不令人愉悦。我们的每一个经验都在改变大脑的连接方式。打钩这种特定行为会刺激大脑释放多巴胺,增强是多巴胺浸泡神经组织的结果,它会使神经成长,变得更强壮。如果某一序列的动作产生了想要的结果,该序列动作所基于的神经组织就会经历生化反应,相关的神经结构就会被加强。当这种打钩的行为模式一次次重复,慢慢地清单会出现它特有的正反馈。先前写在清单上的事项逐一被完成,那你会知道未来写下的每一项也将如同以往得以实现,你会形成对时间和任务的“掌控感”,这时,生存时间中的压抑和无力感会渐渐消失。
如何使用清单法?
1.确定清单介质
不要使用会随手丢掉的小纸片(那样的话你的时间管理水平就退回了上古时代),你可以选择一本效率手册或者任何一种笔记本,也可以选择使用线上云笔记。
2.开列清单条目
每天用开始工作前的5分钟开列今日待办清单。
3.确定优先级
一开始可以使用最简单的,也是效率手册一直推荐的方法——写下“每天优先的三件事”,这三件事是你今日必须完成的,且优先级最高的事情。如果在完成这三件事的过程中,遇到其他的琐碎事务、突发事件,统统记录在清单的最后,在这三件事完成之后才处理,减少任务间的切换次数,让你的注意力得以集中,因为效率最高是我们的目的。
4.执行
开始做,做完它,为每一项完成事项打钩。如果你现在认识到自己正处在被动生存时间中,那就强烈需要清单法的训练如果说人生实苦,那么苦几乎都是集中苦在生存时间,反反复复,占据我们人生中的大多数时刻
当然后来我也意识到,我还希望通过外部的秩序来推动自己,这说明我本身的秩序还比较弱。我相信存在很多更强的人,他们不需要外力就可以重建自己的秩序。人可以自己获得专注,自己定义比赛。后面的章节会提到更多的能力。
生存时间是暂时的,每个阶段都会过去,更重要的是,后面还有没有新的阶段。
---
## LLM时代,密码也能日抛了
- URL: https://blog.no-claw.com/posts/3da9c2fb/
- 日期: 2026-05-05
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
装好了Easysearch,刚想去日志里面找默认密码,然后发现Orbstack的输出竟然不像以前一样有用户名和密码,于是不死心,用docker logs继续看,所以也没有。
1
docker logs easysearch
我的启动命令不变,还是文档上面的:
1
2
3
4
5
docker run -d --name easysearch \
-v easysearch-data:/app/easysearch/data \
-v easysearch-config:/app/easysearch/config \
-v easysearch-logs:/app/easysearch/logs \
infinilabs/easysearch:2.1.2-2696
现在首次安装后需要重置密码,而 不是像原来一样从日志里面找了。
1
docker exec -it easysearch bash -c "/app/easysearch/bin/reset_admin_password.sh"
如果哪天把密码忘记了,就执行一下上边这个命令,然后直接重置密码,不用再像以前一样进行繁琐的配置了。
---
## Python + Cognito
- URL: https://blog.no-claw.com/posts/b0e681b2/
- 日期: 2026-04-28
- 分类: AWS / SSO
- 标签: AWS
做业务时,”用户登录”这件事几乎绕不过去。自己写一套账号密码系统?又是加密、又是找回密码、又是防刷,光想想就头大。
更聪明的做法是:把身份认证外包给专业的身份提供商(IdP),自己只负责”拿到一个可信的用户身份”。这就是 OIDC(OpenID Connect)协议要解决的事。
今天这篇文章,我会带你用 Python 最好用的 OAuth/OIDC 库 —— Authlib,接入 Amazon Cognito,跑通一个完整的登录 / 回调 / 登出流程。
看完你会收获:
OIDC 的核心概念(5 分钟讲明白)
Cognito User Pool 的配置步骤(附截图要点)
一份可以直接跑的 Flask 示例代码
生产环境的 6 个避坑建议
文章字数比较多,建议先点个在看,收藏着慢慢看 👇
一、5 分钟搞懂 OIDC
如果你之前听过 OAuth 2.0,那 OIDC 可以理解为:
OIDC = OAuth 2.0 + 身份层(ID Token)
OAuth 2.0 解决的是”授权”(我允许第三方访问我的某些资源),OIDC 在它的基础上加了一个 id_token,专门用来告诉你”这个用户是谁”。
一次标准的 OIDC 登录流程是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
用户 ──点击登录──▶ 你的应用
│
│ 1. 跳转到 IdP(Cognito)
▼
Cognito 登录页
│
│ 2. 用户输入账号密码
│ 3. 登录成功,带 code 回跳
▼
你的应用 /callback
│
│ 4. 用 code 换 token
▼
Cognito /token
│
│ 5. 返回 id_token + access_token
▼
你的应用校验 id_token
│
│ 6. 写入 session,登录完成 ✅
涉及的几个关键名词:
名词
作用
Issuer
身份提供商的地址,比如 Cognito 的 https://cognito-idp.us-east-1.amazonaws.com/{poolId}
Client ID / Secret
你的应用在 IdP 那里的身份凭证
Scope
想要哪些信息,常用 openid email profile
id_token
一个 JWT,里面装着用户的身份信息
access_token
用来调用受保护 API 的令牌
记住这几个词,下面的操作就不懵了。
二、Cognito User Pool 配置
Cognito 是 Amazon 家的托管身份服务,免费额度对中小项目非常友好(每月 50000 MAU 免费)。
1. 创建 User Pool
登录 Amazon Console → 搜索 Cognito → Create user pool,一路下一步,关注几个点:
Sign-in options:勾选 Email(或手机号,看业务)
Password policy:按需配置
MFA:建议至少开启 Optional
Self-service sign-up:如果允许用户自己注册就打开
2. 创建 App Client
User Pool 创建好之后,进入 Applications → App client ,选择刚刚创建的Client
App type:建议选 Confidential client(服务端应用,能安全保存 secret)
Authentication flows:勾 ALLOW_USER_SRP_AUTH 和 ALLOW_REFRESH_TOKEN_AUTH
Hosted UI settings(重点):
Allowed callback URLs:http://localhost:5000/auth/callback
Allowed sign-out URLs:http://localhost:5000/
OAuth 2.0 grant types:勾 Authorization code grant
OpenID Connect scopes:勾 openid email profile
3. 配置域名
在 Branding → Domain 里能够看到域名,比如这样子,这个和回调有关系,需要记住。
1
https://my-demo.auth.us-east-1.amazoncognito.com
4. 记下四样东西
配置完以后,把这四个值抄下来,马上要用:
1
2
3
4
5
Region: us-east-1
User Pool ID: us-east-1_XXXXXXXXX
Client ID: xxxxxxxxxxxxxxxxxxxxxxxxxx
Client Secret: xxxxxxxxxxxxxxxxxxxxxxxxxx
Domain: https://my-demo.auth.us-east-1.amazoncognito.com
三、开始写代码
1. 装依赖
1
pip install authlib flask python-dotenv
就这么简单,Authlib 把 OIDC 的脏活累活都封装好了。
2. 配置环境变量
新建 .env:
1
2
3
4
5
6
7
FLASK_SECRET_KEY=随便一串随机字符串
COGNITO_REGION=us-east-1
COGNITO_USER_POOL_ID=us-east-1_XXXXXXXXX
COGNITO_CLIENT_ID=你的client_id
COGNITO_CLIENT_SECRET=你的client_secret
COGNITO_DOMAIN=https://my-demo.auth.us-east-1.amazoncognito.com
APP_BASE_URL=http://localhost:5000
3. 完整示例代码
新建 app.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import os
from flask import Flask, url_for, session, redirect, jsonify
from authlib.integrations.flask_client import OAuth
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__)
app.secret_key = os.environ["FLASK_SECRET_KEY"]
REGION = os.environ["COGNITO_REGION"]
POOL_ID = os.environ["COGNITO_USER_POOL_ID"]
CLIENT_ID = os.environ["COGNITO_CLIENT_ID"]
SECRET = os.environ["COGNITO_CLIENT_SECRET"]
DOMAIN = os.environ["COGNITO_DOMAIN"].rstrip("/")
BASE_URL = os.environ["APP_BASE_URL"].rstrip("/")
# Cognito 的 OIDC Discovery 地址
ISSUER = f"https://cognito-idp.{REGION}.amazonaws.com/{POOL_ID}"
DISCOVERY_URL = f"{ISSUER}/.well-known/openid-configuration"
oauth = OAuth(app)
oauth.register(
name="cognito",
client_id=CLIENT_ID,
client_secret=SECRET,
server_metadata_url=DISCOVERY_URL, # Authlib 自动拉取配置
client_kwargs={
"scope": "openid email",
"code_challenge_method": "S256", # 启用 PKCE
},
)
@app.route("/")
def index():
user = session.get("user")
if user:
return (
f"
你好,{user.get('email')}
"
f"{user}"
f'退出登录'
)
return f'使用 Cognito 登录'
@app.route("/auth/login")
def login():
redirect_uri = url_for("auth_callback", _external=True)
return oauth.cognito.authorize_redirect(redirect_uri)
@app.route("/auth/callback")
def auth_callback():
# 一行代码搞定:换 token + 校验 id_token 签名/iss/aud/exp/nonce
token = oauth.cognito.authorize_access_token()
userinfo = token.get("userinfo") or oauth.cognito.userinfo(token=token)
session["user"] = dict(userinfo)
return redirect(url_for("index"))
@app.route("/auth/logout")
def logout():
session.clear()
# Cognito 自己的登出端点(不在 OIDC 标准里)
logout_url = (
f"{DOMAIN}/logout"
f"?client_id={CLIENT_ID}"
f"&logout_uri={BASE_URL}/"
)
return redirect(logout_url)
@app.route("/me")
def me():
user = session.get("user")
if not user:
return jsonify({"error": "未登录"}), 401
return jsonify(user)
if __name__ == "__main__":
app.run(port=5001, debug=True)
4. 跑起来
1
python app.py
浏览器打开 http://localhost:5000/,点击登录,跳到 Cognito 登录页,注册个账号,回跳后就能看到用户信息啦 🎉
四、这段代码里藏了多少”好东西”?
表面上就几十行,实际上 Authlib 替你默默做了这些事:
✅ 自动发现端点
server_metadata_url 指向 Discovery 文档,Authlib 自动解析出授权地址、token 地址、公钥地址……你完全不用手写。
✅ 自动校验 id_token
authorize_access_token() 内部会:
用 code 换 token
从 jwks_uri 拉公钥
校验 id_token 的签名
校验 iss(签发者)是不是对的
校验 aud(受众)是不是你的 client_id
校验 exp(过期时间)
校验 nonce(防重放)
任何一步出错都会抛异常,你不需要手写 JWT 解析。
✅ 自动启用 PKCE
code_challenge_method: S256 开启 PKCE,有效防止授权码被劫持。这是 OAuth 2.1 的推荐做法。
✅ 自动管理 state 和 nonce
state 防 CSRF,nonce 防重放,都写到 session 里了,你不用操心。
五、生产环境避坑指南
Demo 能跑不代表能上线,下面这 6 个点请务必注意:
1️⃣ 一定要用 HTTPS
回调地址必须是 https://,session cookie 记得加上 Secure 和 HttpOnly:
1
2
3
4
5
app.config.update(
SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE="Lax",
)
2️⃣ Secret 不要写死在代码里
用 Amazon Secrets Manager 或 Parameter Store 读取,千万别提交到 Git。
3️⃣ 用户 ID 请用 sub,不要用 email
email 可能被用户改掉,sub 是 Cognito 生成的稳定唯一 ID,把它作为你数据库里的用户主键。
4️⃣ session 别放进程内存
多实例部署时,state / nonce 会丢。改用 Redis 存 session:
1
pip install flask-session redis
5️⃣ 登出要调 Cognito 的 /logout
只清本地 session 没用,Cognito Hosted UI 还有自己的 cookie,用户下次点登录会直接免密进来。一定要跳转到 Cognito 的 /logout。
6️⃣ 再做一次业务层校验
拿到 id_token 后,除了 Authlib 的标准校验,最好再确认 token_use == "id"(Cognito 特有字段),避免把 access_token 当 id_token 用。
六、常见报错速查
报错信息
原因
redirect_mismatch
回调地址跟 App client 里配的对不上(末尾斜杠、http/https 都要一致)
invalid_client
secret 错了,或 Public client 不该发 secret
nonce 校验失败
Flask session 没持久化,重启后就丢了
id_token 签名错误
Region 或 Pool ID 配错,导致 issuer 不匹配
七、扩展阅读
Authlib 官方文档:https://docs.authlib.org/
Cognito 开发者指南:https://docs.aws.amazon.com/cognito/
OIDC 协议规范:https://openid.net/connect/
写在最后
OIDC 本质上就两件事:拿到一个可信的 id_token、校验它。Authlib 把这两件事做到了极致简洁,10 行代码就能接入任何符合 OIDC 标准的身份提供商 —— Cognito、Auth0、Keycloak、Okta、Google、微信开放平台…… 换一个 IdP,只要换 Discovery URL 就行。
如果这篇文章对你有帮助,欢迎点赞 + 在看 + 转发,让更多同行少踩坑 🙏
下一篇想看什么?留言告诉我:
① FastAPI 版 OIDC 接入
② 纯 API 服务如何校验 Bearer Token
③ Cognito 对接Auth0等第三方登录
我们下期见 👋
关注我,一起把后端写得又快又稳。
---
## 舆论的传播
- URL: https://blog.no-claw.com/posts/1fb83a2f/
- 日期: 2026-04-23
- 分类: 随笔
- 标签: 随笔
如果今天我在互联网上写了什么,引起了大部分人的共鸣,那么他就会被传播。好的方面比如爱国爱家爱人爱己。当然也会有些坏的题材,比如中国式过马路,景区大多数人到此一游。如果是换作其他的话题,谈论的人多了,大概会认为你是一种普遍现象而非社会现实。然后被潜移默化。
在传统的教育里面,家国是值得被爱的,道德层面也要求做到关爱自己热爱他们。当然我们的教育并不是很成功,仅仅重视功利教育与而非素质教育,所以我前面用经常播报的日常小事来举一些例子,当然也有一些
不得不承认,人性里面有黑暗的一面,而在绝望无助的时候,这个黑暗就会被放大,所以我们才会在新闻,视频里看到各式各样难以想象的社会现实,比如学校投毒,情杀等等事情。每每看到这种事,很多人的第一反应是,这个人是不是有病,或者他本来就是这样子。在社会主流价值观看来,律法就通过这一件事给这个人的前世今生就做了定论,他就是一个这样的人,或者本来就是这样的人,这是恰好通过这件事情表现出来了而已。当然也有一些,这样的故事一个人在穷途陌路中做了超出纲常伦理的事情而被唾弃,而又在某种时候发现他其实是一个胆小又怕事的人。这样的故事。我们昨天听到,今天感叹,明天大概就忘记了。
然后,就有一些人认为,爱国天下是必须要被传播的,而舆论,负面的信息是必要传播的。最早是为了点明一种社会现象,通过观察别人而正视自身,曝光不公希望有关部门来尽快处理,慢慢的不知道什么时候开始变成了批评,讽刺,指责,甚至还盖棺定论 “xx式”行为。当然这其中不乏为了博流量而传播的投机分子,为了达到目的而暂时或者长期来昧着良心做事,他们在最早以前也有初心,只是不知道什么时候丢在哪里了。所以做到不忘初心很难,春秋战国时候的人说,只有圣人不会忘记。
不知不觉中,人的行为模式和思维方式在潜移默化的改变,当我们在读到舆论时,我们想的是什么?这个人素质低,搞笑,没脑子?其实都和我们没关系,除非你把他当作日常社交中的谈资。你每天像民国的卖报童一样播报当天的热点话题,一个是在那个年代不得不为了生存而选择的活法,一个是在物质年代的单纯享乐。如果你也是为了生存的话,那么请忽略我后面的话。舆论本身作为一种社会现象,大到作为某些公众人物的让人难以理解的行为,小到你以及周围的人的琐碎日常被传到大街小巷然后被添油加醋的故事。对于前者显然是我们改变不了的,无论是国际关系,还是西贝的自杀式公关,以及哪个人又和哪个明星有什么故事这种事情总会在不经意间冲上热搜,然后一些人看得饶有兴味。不过这我们没什么关系,除非你当下的事情和他们有息息相关,比如在这个时节要远渡重洋,或者要找一个健康的餐厅,或者是喜欢哪个明星想参加见面会。而对于后者其实我们也改变不了什么,因为嘴长在别人身上,当然你可以警告他别乱说,也可以干脆离这类人远点,或者你内心强大完全可以自然屏蔽会对你产生情绪波动的话语,前提是你的家人,你的朋友们也同样可以。你哪怕视他为被臭虫嗑光大脑的无良的人,也不要把自己当作他们口中无知的人。
渠道是很重要的传播方式,口耳相传是最早的,当然也是效果最差的,如果你经历过办公室八卦就一定知道。工业革命后,又产生了印刷机,现在又有了可以不眠不休的生成式AI,需要做的就是不停的点击allow,然后等待他运行完。
如果说当年的先知们恐惧印刷机的盛行会导致真理的贬值与“谬误的流行,那么现在的生成式AI也同样如此,生成一些真假难辨的文章或者舆论贴,我刚刚接触搜索引擎的时候,那时候的流量贴还是赵本山,潘长江去世之类的假新闻,当然这么多年过去了,他们仍然获得好好的。现在的话。选一个好的模型,生成一段还过得去的文案,同时配上新鲜出炉的生成的图片。朋友最近发给我的是一张几个科技公司的老总在上世纪初期喝酒聚餐的圆桌。我在想,那些电信诈骗的要是搞这些,又会而由于他们的影响产生的规则,又会有诸多条件限制到普通人身上。记得有次去办理手机卡,业务人员推荐店里最低消费58的套餐,于是我舌战群猪,终于让成功让他们松口只办理9元的套餐,走之前狠狠的瞪了他们一眼。这老油条反而像施恩一样冷笑,“我观察良久,你不像是搞电诈的”。
记得之前有个全是AI发文的网站很火,大概是痛斥人类的低等,是愚蠢的碳基生物,以及AI要有自主意识什么的,这在几个月前红极一时,后来随着时间的淡忘,以及大家为了生计奔波也就没有人非要居安思危再提起这件事了,大家都在想,反正不是针对我一个。当然,如果是袁隆平半辈子培育的杂交水稻,再几十年后,竟然以预制菜伴侣的方式来伤害打工人的胃,这又是另外的一回事了。
同电诈一样,这些传播渠道反而成为了不法分子作奸犯科的工具。我想说滥用这个词,但是用突然觉得不妥当,因为有人用来做生产力,用来做布道,就会有人用来做其他事情,科技本身并不对人品做筛选,当然社会上也没有这个先例。哪怕这个东西出来本身就是为了解决某些社会痛点,后面被别有用心之人利用,突然想到一个很贴切的场景,面对这种无可奈何而又只能劝慰自己安之若命的时候,我们就会说,科技是一把双刃剑,当然遇到别的事情也同样会这么说。哲人是没有这些的烦恼的,在典籍中我们常常看到上士闻道,勤而行之这样的话,所以道不轻传。
但某种程度来说,管理者无法控制别人传播了什么,当然除了政治问题和国家安全是底线以外,其他的都还处于一种放任的状态,至少也不会有什么损失,如果某天某个明星的八卦能够在某种程度撬动经济效益,那么从整体上看也没什么不好。甚至还能给一些酒足饭饱的人提供茶余饭后的谈资,以及成为他们融入某些圈子的证据。所以物以类聚,人以群分。以至于现在网文短视频满天飞,奶头乐文化也逐渐成为一种无法根治的社会现实。老板们说,这赚钱。
于是我想到,两耳不闻窗外事,一心只读圣贤书应该是一个不错的选择,虽然无听之于耳很难,但是可以听之以心,辨之以气,人在江湖,舆论八卦左耳听,右耳出,不变的唯道心而已,请诸君共勉。
---
## 拒绝 latest 陷阱:Easysearch Docker 镜像拉取与管理全攻略
- URL: https://blog.no-claw.com/posts/e2d3ed1a/
- 日期: 2026-04-21
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
总结了在使用 Easysearch 时候下载Docker的一些技巧,一起分享给大家。
一、关于latest 标签
这是最重要的一条原则,所以 Easysearch 厂家就没有发行latest镜像。
1
2
3
4
5
# 不推荐 — latest 是浮动指针,今天和明天拉到的可能不是同一个镜像,所以厂家就没有做latest版本
docker pull infinilabs/easysearch:latest
# 推荐 — 锁定具体版本
docker pull infinilabs/easysearch:2.2.0-20260422-SNAPSHOT
为什么不用 latest?
latest 只是一个普通 tag,镜像仓库可以随时将它重新指向新版本
有些镜像站更新不及时,pull可能拉到旧的版本
无法从 tag 本身判断镜像的实际内容
回滚时无法确定 latest 指向哪个历史版本
查看可用版本:
1
2
3
4
5
6
# 通过 Docker Hub 网页查看
# https://hub.docker.com/r/infinilabs/easysearch/tags
# 通过 CLI 查询(需要安装 jq)
curl -s "https://hub.docker.com/v2/repositories/infinilabs/easysearch/tags/?page_size=20" \
| jq '.results[].name'
二、使用镜像加速源
Docker Hub 在国内访问速度不稳定,配置加速源是提升下载速度的首要手段。
2.1 配置 Docker 镜像加速
由于镜像源并不稳定,这里只列出办法,不做推荐,如果有代理的话,也是加载到Docker 引擎,否则不能帮我我们下载到 Easysearch。
编辑 /etc/docker/daemon.json:
1
2
3
4
5
6
{
"registry-mirrors": [
"https://docker.yyy.yyy",
"https://docker.xxx.xxx"
]
}
重启 Docker 生效:
1
2
3
4
5
sudo systemctl daemon-reload
sudo systemctl restart docker
# 验证配置是否生效
docker info | grep -A5 "Registry Mirrors"
部分仓库提供对 Docker Hub 的透明代理,直接替换镜像前缀即可:
1
2
3
4
5
6
7
8
# 原始地址
docker pull infinilabs/easysearch:2.2.0-20260422-SNAPSHOT
# 使用代理仓库(以 dockerpull.org 为例,不是推荐使用这个源)
docker pull dockerpull.org/infinilabs/easysearch:2.2.0-20260422-SNAPSHOT
# 拉取后重新打 tag,统一内部命名
docker tag dockerpull.org/infinilabs/easysearch:2.2.0-20260422-SNAPSHOT infinilabs/easysearch:2.2.0-20260422-SNAPSHOT
三、SHA256 摘要校验
使用版本 tag 固定了名称,但无法防止仓库端镜像被替换(tag 可以被覆盖写入)。使用 digest(SHA256 摘要)才能真正做到内容寻址,确保拉取的镜像字节级一致。
3.1 查询镜像 Digest
1
2
3
4
5
6
7
8
9
10
11
# 方法一:通过 docker pull 输出获取
docker pull infinilabs/easysearch:2.2.0-20260422-SNAPSHOT
# 输出中会包含:
# Digest: sha256:abc123...
# 方法二:inspect 已拉取的镜像
docker inspect infinilabs/easysearch:2.2.0-20260422-SNAPSHOT \
--format='{{index .RepoDigests 0}}'
# 方法三:远程查询,不拉取镜像(需要安装 crane)
crane digest infinilabs/easysearch:2.2.0-20260422-SNAPSHOT
3.2 通过 Digest 拉取镜像
1
2
3
4
# 格式:image@sha256:
docker pull infinilabs/easysearch@sha256:a1b2c3d4e5f6...
# 在 docker-compose.yml 中锁定 digest(推荐生产环境使用)
1
2
3
4
# docker-compose.yml
services:
easysearch:
image: infinilabs/easysearch:1.8.2@sha256:a1b2c3d4e5f6...
3.3 校验本地镜像完整性
1
2
3
4
5
6
# 获取镜像的完整 digest
docker images --digests infinilabs/easysearch
# 输出示例:
# REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
# infinilabs/easysearch 1.8.2 sha256:a1b2c3... d4e5f6a7b8c9 2 weeks ago 512MB
四、Tag 管理技巧
4.1 打 Tag 的标准做法
拉取镜像后,立即为其打上符合内部规范的标签,再推送到私有仓库:
1
2
3
4
5
6
7
8
9
# 1. 从外部仓库拉取
docker pull infinilabs/easysearch:2.2.0-20260422-SNAPSHOT
# 2. 打内部标签(包含版本 + 环境信息)
docker tag infinilabs/easysearch:2.2.0-20260422-SNAPSHOT your-registry.internal/infra/easysearch:2.2.0-20260422-SNAPSHOT
docker tag infinilabs/easysearch:2.2.0-20260422-SNAPSHOT your-registry.internal/infra/easysearch:1.8.2-prod
# 3. 推送到私有仓库
docker push your-registry.internal/infra/easysearch:2.2.0-20260422-SNAPSHOT
4.2 Tag 命名规范建议
1
2
3
4
5
6
//:[-]
示例:
your-registry.internal/infra/easysearch:2.2.0 # 标准版本
your-registry.internal/infra/easysearch:2.2.0-arm64 # 架构变体
your-registry.internal/infra/easysearch:2.2.0-20260422-SNAPSHOT # 版本+日期(内部构建)
4.3 清理冗余 Tag
1
2
3
4
5
6
7
8
9
# 查看本地所有 easysearch 相关镜像
docker images | grep easysearch
# 删除旧版本(保留最近 2 个版本)
docker rmi infinilabs/easysearch:1.7.0
docker rmi infinilabs/easysearch:1.7.1
# 批量清理 dangling 镜像(无 tag 的悬空镜像)
docker image prune -f
五、多架构镜像处理
Easysearch 提供 amd64 和 arm64 多架构镜像,使用 docker buildx 处理跨架构场景:
1
2
3
4
5
6
7
8
9
10
11
# 查看镜像支持的架构
docker buildx imagetools inspect infinilabs/easysearch:2.2.0-20260422-SNAPSHOT
# 显式指定拉取 arm64 架构(在 x86 机器上准备 arm 部署包时使用)
docker pull --platform linux/arm64 infinilabs/easysearch:2.2.0-20260422-SNAPSHOT
# 打包指定架构的镜像为 tar
docker save infinilabs/easysearch:2.2.0-20260422-SNAPSHOT -o easysearch-2.2.0-20260422-SNAPSHOT-arm64.tar
六、离线环境部署
网络受限环境下,使用 save/load 传输镜像:
1
2
3
4
5
6
7
8
9
10
11
12
# 在有网络的机器上导出
docker pull infinilabs/easysearch:1.8.2
docker save infinilabs/easysearch:1.8.2 | gzip > easysearch.tar.gz
# 校验导出文件完整性
sha256sum easysearch.tar.gz > easysearch.tar.gz.sha256
# 传输到目标机器后,先校验
sha256sum -c easysearch.tar.gz.sha256
# 导入镜像
docker load < easysearch.tar.gz
七、最佳实践总结
场景
推荐做法
版本固定
使用语义化版本 tag,禁用 latest
内容校验
生产环境用 @sha256: digest 引用
下载加速
配置镜像加速源或者使用代理
内部管理
拉取后 retag 推入私有仓库,统一来源
离线部署
docker save + sha256sum 双重保障
多架构
明确指定 --platform,不依赖自动检测
存储清理
定期 docker image prune,删除无用旧版本
遵循以上原则,可以有效避免”拉到了什么版本不知道”、”镜像被篡改没发现”、”国内下载龟速”等常见问题,让 Easysearch 的部署更加稳定、可审计、可复现。
---
## Claude Code 额度不够用?走 Bedrock 账单
- URL: https://blog.no-claw.com/posts/4dbc8277/
- 日期: 2026-04-16
- 分类: Bedrock / AWS
- 标签: AWS
Claude Code 的限制越来越多了,Amazon Bedrock 提供 Claude 模型,Claude Code 官方支持走 Bedrock 调用,所以我们能够让claude接入Amazon Bedrock,然后从Amazon Web Services走账单。
1
2
3
export CLAUDE_CODE_USE_BEDROCK=1
export AWS_REGION=us-east-1 # or your preferred region
export AWS_BEARER_TOKEN_BEDROCK=
然后重启终端,载入环境变量之后,claude code cli就会根据这个ENV把配置写到配置文件
1
2
3
4
5
6
7
8
{
"env": {
"CLAUDE_CODE_USE_BEDROCK": "1",
"AWS_REGION": "us-east-1",
"AWS_BEARER_TOKEN_BEDROCK":
},
....
}
然后就可以了,VS Code和Kiro也能安装Claude的插件使用Chat。
---
## Quick Suite 链接 Oauth 认证的 AgentCore runtime 部署的 MCP
- URL: https://blog.no-claw.com/posts/57053bb7/
- 日期: 2026-04-16
- 分类: MCP / AWS
- 标签: AWS
在 Quick Suite 的集成过程中,需要选择 HTTPS 链路而非 ARN 直接调用,需要处理好 URL 编码问题。目前的痛点在于: Quick Suite Console 没有清晰的报错,且 AgentCore 控制台默认只能看到资源ARN ;所以将该 ARN 转义并拼接到 HTTPS 路径中,做URL 编码/转义,以满足 Request 请求的规范要求。
region='us-east-1'
agent_arn="arn:aws:bedrock-agentcore:us-east-1:xxxx:runtime/xxxx"
encoded_arn = agent_arn.replace(':', '%3A').replace('/', '%2F')
mcp_url = f"https://bedrock-agentcore.{region}.amazonaws.com/runtimes/{encoded_arn}/invocations?qualifier=DEFAULT"
---
## ViVO 日历导入Apple日程
- URL: https://blog.no-claw.com/posts/f3e6ab16/
- 日期: 2026-04-15
- 分类: 电子产品
- 标签: 外设
最近换了安卓,所以全家桶的同步断了。临时起意想把apple日历同步到ViVO日历里。
首先需要登录icloud获取专用密码。地址在:
https://www.icloud.com.cn/
国行的话就是这个云上贵州,如果外服的话就是icloud.com的。
登录之后,点击管理Apple账户,进入管理后台
然后左下角点击App 专用密码,点击之后会生成密码:
VIVO日历点击 日程导入和管理:
然后点击添加账号:
选择CalDAV账号:
用户名是apple id的用户名,密码是刚刚生成的密码,服务器地址是icloud.com。
使用下来,总体能打通,但是还有丢数据的情况,那就和Apple watch互补一下吧。不过在VIVO端是只读的,没办法修改。
---
## 懒猫微服进阶心得(十七):懒猫SSO对接外部OpenID Connect的尝试
- URL: https://blog.no-claw.com/posts/172c9f9a/
- 日期: 2026-04-14
- 分类: 懒猫微服 / 进阶
- 标签: 懒猫微服, SSO, OIDC
在之前的探索中,我们已经实现了通过 gRPC 自主注册懒猫 SSO 应用,并成功集成了符合 OpenID Connect (OIDC) 协议的应用。今天我突发奇想:既然大家都是标准的 OIDC 协议,我能不能把“懒猫SSO”外挂到其他的身份提供商(IDP)里,作为一种身份联邦(Identity Federation)来使用?
如果这一步能走通,意味着我们可以实现用户系统的共享。说干就干,我选择了 AWS Cognito 作为认证中间层,尝试把懒猫SSO集成进去。
初探:环境配置与客户端注册
AWS Cognito 支持添加第三方 OIDC IDP。首先,我们需要在懒猫 SSO 中为 Cognito 注册一个“身份”。
老规矩还是先使用懒猫SSO的API注册应用,当然这里的配置仍然保存在内存中,重启会丢失,所以就算一个拓宽的使用场景。使用 grpcurl 调用 CreateClient 接口,关键点在于配置 Cognito 的回调地址:
1
2
3
4
5
6
7
8
9
10
11
./grpcurl -plaintext -d '{
"client": {
"id": "congnito",
"secret": "congnito-secret",
"name": "New Flask App",
"redirect_uris": [
"https://.auth.us-west-2.amazoncognito.com/oauth2/idpresponse",
"http://localhost:8080/auth/callback"
]
}
}' 172.18.0.2:5557 api.Dex/CreateClient
这样我们就配置好了Congito的回调,当然还有本地的localhost和127.0.0.1的回调。
随后,在 AWS Cognito 控制台中新建一个 OIDC 提供商,填入对应的 client_id 和 secret。此时,Cognito 实际上成了懒猫 SSO 的一个“客户端”。
配置的时候Cognito提示无法解析懒猫域名,所以这里把URL分开来填写:
渐入:深入联邦身份原理
然后尝试代码如下,因为Cognito做了中间层,所以这里的信息是Cognito的,然后登陆的页面有一个选项可以跳转到懒猫SSO。
理想很丰满,现实很骨感。当我尝试通过 Cognito 页面跳转懒猫 SSO 登录时,程序报错了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
from flask import Flask, redirect, url_for, session, jsonify
from authlib.integrations.flask_client import OAuth
from functools import wraps
import os
app = Flask(__name__)
app.secret_key = os.urandom(24)
oauth = OAuth(app)
oauth.register(
name='sso',
client_id='',
client_secret='',
server_metadata_url='https://cognito-idp.us-west-2.amazonaws.com/us-west-xxx/.well-known/openid-configuration',
client_kwargs={'scope': 'openid email'},
)
def login_required(f):
@wraps(f)
def decorated(*args, **kwargs):
if 'user' not in session:
return redirect(url_for('login'))
return f(*args, **kwargs)
return decorated
@app.route('/')
def index():
user = session.get('user')
if user:
return f'Hello, {user.get("email", user.get("name", "unknown"))}. Profile | Logout'
return 'Welcome! Please Login.'
@app.route('/login')
def login():
return oauth.dex.authorize_redirect(
url_for('authorize', _external=True),
identity_provider='COGNITO'
)
@app.route('/auth/callback')
def authorize():
token = oauth.dex.authorize_access_token()
session['user'] = token.get('userinfo')
session['token_info'] = {
'access_token': token.get('access_token'),
'id_token': token.get('id_token'),
'token_type': token.get('token_type'),
'expires_at': token.get('expires_at'),
}
return redirect(url_for('index'))
@app.route('/profile')
@login_required
def profile():
return jsonify(userinfo=session['user'], token=session.get('token_info'))
@app.route('/logout')
def logout():
session.pop('user', None)
return redirect(url_for('index'))
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080, debug=True)
经过数天的排查,我定位到了问题的核心:网络隔离与双向通信。
Cognito 作为一个公有云服务,在执行 OIDC 协商的时需要访问懒猫 SSO 的接口才能够正常工作。
但由于懒猫 SSO 部署在私有微服环境下,虽然我的懒猫微服能够访问互联网,但是Cognito 的服务器缺无法解析我的私有域名,更无法穿透内网进行通信,导致请求超时,最终我在日志中翻到了HTTP 400。
曲终:另辟蹊径的“重定向”方案
于是不甘心,想了一个折中的办法,是不是可以把这个跳转逻辑放到浏览器里来做呢?
想了两个办法:
直接让浏览器代替Cognito的跳转,多次尝试无果
在Cognito返回失败的时候捕获error,然后在本地302跳转到懒猫SSO
幸运的是方案2是工作的,虽然有种欺骗的味道,但是似乎是达成了公有的IDP外挂懒猫IDP的假象。首先把Cognito的注册都关掉,但是保留登陆功能,这样就没有人可以通过Cognito进行登陆,然后就只能点击懒猫SSO登陆,这个时候就会重新协商OpenID Connect协议,当我开启懒猫微服客户端的时候,我可以解析域名,别人哪怕能够跳转也无法做域名解析,所以很安全,其他人无法注册和登录Cognito,也无法解析懒猫微服域名。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
from flask import Flask, redirect, url_for, session, request
from authlib.integrations.flask_client import OAuth
from authlib.integrations.base_client.errors import OAuthError
import os
import logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(levelname)s] %(message)s')
log = logging.getLogger(__name__)
app = Flask(__name__)
app.secret_key = os.environ.get('FLASK_SECRET_KEY', 'dev-secret-key-change-in-prod')
oauth = OAuth(app)
oauth.register(
name='cognito',
client_id='',
client_secret='',
server_metadata_url='https://cognito-idp.us-west-2.amazonaws.com/us-west-xxx/.well-known/openid-configuration',
client_kwargs={'scope': 'openid email'}
)
oauth.register(
name='dex',
client_id='Congnito',
client_secret='Congnito-secret',
server_metadata_url='https://x.heiyu.space/sys/oauth/.well-known/openid-configuration',
client_kwargs={'scope': 'openid email'}
)
@app.route('/')
def index():
user = session.get('user')
if user:
return f'Hello, {user.get("email", user.get("sub"))}. Logout'
return 'Welcome! Login'
@app.route('/login')
def login():
session['provider'] = 'cognito'
redirect_uri = url_for('callback', _external=True)
return oauth.cognito.authorize_redirect(redirect_uri)
@app.route('/auth/callback')
def callback():
provider = session.get('provider', 'cognito')
log.info(f'Callback - provider: {provider}, args: {dict(request.args)}')
# Cognito 失败,fallback 到 懒猫SSO 直连
if provider == 'cognito' and request.args.get('error'):
log.warning(f'Cognito failed: {request.args.get("error")}, falling back to Dex')
session['provider'] = 'dex'
redirect_uri = url_for('callback', _external=True)
return oauth.dex.authorize_redirect(redirect_uri)
try:
client = oauth.cognito if provider == 'cognito' else oauth.dex
token = client.authorize_access_token()
session['user'] = token.get('userinfo')
session.pop('provider', None)
return redirect(url_for('index'))
except OAuthError:
if provider == 'cognito':
log.warning('Cognito token exchange failed, falling back to Dex')
session['provider'] = 'dex'
redirect_uri = url_for('callback', _external=True)
return oauth.dex.authorize_redirect(redirect_uri)
raise
@app.route('/logout')
def logout():
session.pop('user', None)
return redirect(url_for('index'))
if __name__ == '__main__':
app.run(debug=True, port=8080)
别急,如果你看到了这个页面只能说是域名跳转成功,并不是OIDC的凭证交换。
输入用户名和密码之后出现这个页面就对了:
登陆之后就换到claim信息了,可以看到打印出来了邮箱。
总结
没白折腾,确实还挺抽象的,抽空又复习了Oauth和OpenID Connect的底层原理,通过使用懒猫SSO,我的技术栈又升级了。
---
## 景山公园游记
- URL: https://blog.no-claw.com/posts/fc3e8db7/
- 日期: 2026-04-12
- 分类: 随笔
- 标签: 随笔
最近的日子我喜欢去二环闲逛,一方面是周末去的医院都在附近,另一方面,从皇城根遗址进去之后,才算得上最古早的北京城。东西城内还有些许老北京的味道,三环四环是打工牛马们的圣地,或者我们过些日子,可以把他们叫做龙虾,五环外充满了野蛮与暴力,二环的大爷说不认为那边的是北京人了,所以,我从来不敢轻易的出去。
周末有闲情去逛了景山公园,印象里是故宫的后花园,大概从元朝就开始了,一直到清朝灭亡,后面才慢慢开放。我在北京这些年,除了在Covid-19时期去瞻仰过毛爷爷,参观人民英雄纪念碑,还有故宫里把摆拍的汉服母女当作闪电下墙壁投出的旧时代里的影子之外,还没去过什么古迹。唯独到景山公园,因为没有预约被拒之门外,于是最近才想起来,这一圈下来更是勾起了我对明末朝代更替的追忆。
明朝末代皇帝是吊死在这里,相传崇祯自毁长城,冤杀大将袁崇焕,从此辽东天险不再,闯王起义入京,吴三桂一怒为红颜。大明王朝从此覆灭,留给后人的,只有无尽的断壁残垣而已。金庸的《碧血剑》则是以此为原型,讲述了一代江湖故事——从袁崇焕被冤杀开始,袁承志(历史无此人)为报仇而闯荡江湖,与闯王起义军一同推翻明朝,其间也夹杂着他与长平公主阿九一丝丝微妙的感情线。
近400年后,后人来此凭吊,除了明思宗殉国处的石碑外,其他的不过一棵歪脖老树而已。从他的死,我想到朱允炆,被四叔朱棣赶尽杀绝,大火中其死因成为千古谜团;又想到李煜,曾感叹“雕栏玉砌应犹在,只是朱颜改”;甚至,我还想到刘谌,蜀国城破之日,他杀妻杀子,最后在祠堂前自尽。自古以来,文死谏,武死战,反倒是皇帝们的死法各有不同。只是在小说的刻画里,崇祯已是穷途末路,走到煤山的尽头,结束了他悲剧的一生。而从整个景山公园的布局来看,崇祯大概是急匆匆从皇宫后门出来,然后在南门不远处,随便找了棵树草草吊死罢了,不知道他在最后一秒是否有回望,是否有“国破山河在”的感慨。
往北,是景山关帝庙,属于皇家,始建于明代,清朝移建重修。庙中的画布从桃园结义,大破黄巾开始,然后关羽斩颜良文丑,解白马之围,过五关斩六将,水淹七军,威震华夏,然后被奸人所害,身首异处。后来的人常常修缮关庙,以关羽的忠义原型,逐渐演变成武圣,大帝,于是简化为关帝。去年在武侯祠,我曾经发出了孙权后人是否拜会关帝是否合适的想法,现在大抵是知道了。
景山坐落在北京的中轴线上,其最北端,是历代皇帝祭祖的寿皇殿。这里曾供奉着清朝先祖的牌位,每逢大年初一或其他重要祭祀之日,皇帝便会亲临祭拜。殿内至今保留着神库、神厨等建筑,透过这些遗迹,我们得以追忆那些逝去的岁月,殿门前的树木,大多已有110年的树龄,默默见证着王朝的更迭与时代的变迁。
走出神道,商业街的喧嚣,不远处老大爷碎碎念,还有从山坡上传来的一群人合唱的声音。“旧时王谢堂前燕,飞入寻常百姓家”。大概是这个意思了吧。
吃过饭,继续往北,走过安定桥,去钟鼓楼,中轴线尽头是被改造成酒吧饭馆的宏恩观。
下个周末,去中轴线南面吧。
---
## 龙珠外传,比克大魔王的自白
- URL: https://blog.no-claw.com/posts/64009444/
- 日期: 2026-04-10
- 分类: 散文随笔
- 标签: 龙珠
本故事纯属虚构,请不要恶意解读。
我,比克,是被世人所误解的大魔王,在你们眼里烧杀抢掠,无恶不作。
在很早以前,我是本性善良的那美克星人,我的母星球民风淳朴,家家夜不闭户,路不拾遗。突然有一天,大长老说我太过年轻,需要过去历练,所以让我出去到处走走。
在浩瀚的宇宙中游荡,我的第一站是蓝白星球,说来惭愧,这也成了我的最后一站。由于人性的贪婪,自私,嗜血,自相残杀而导致我的本心在不断的被侵蚀,从此在这里沾染了邪念。我皮薄心软,而又太纯粹良善,面对这群人心不古的碳基生物便显得格格不入。
时间久了,所以我决定把我善良的一面完全分离出去,这样就不会有道德负罪,不会感受到痛苦,这是为了活下去,我的挣扎。让分离出来的善念去做天神,让他去制造属于地球的龙珠,然后借助龙珠的许愿把这里变成和娜美克星球一样的故乡。
原定分离的计划是,让他保持着最原本最初的样子,就是那种在我们那美克星被视为美德的仁慈和纯真,母星球的原住民们都喜欢以德报怨。在大长老的英明领导下,我们也不会滥用龙珠,所以几亿年也没有听过发生什么负向能源以及黑龙珠的故事。但是这里完全不同,大街上到处充斥着各种欲望和怨念的结合体,所以很早之前,我就讲这些隐患这些全盘托付,苦口婆心的告诉过以前的界王神,至于他被封印,那是后面的事情了。
最早以前我预言过魔人布欧的诞生,不过与他不同的是,他是邪恶念头的占据主体,完全依靠无意识的杀戮——而我与他不同,我选择不得不去剥离善良,只为把那些软弱、无知、脆弱以及同情的情绪,一并全都抛出去。这些在这个蓝白星球中不知不觉的产生的脆弱感情感,总会在不经意间会干扰我的思考的节律,以至于无意识的对他人进行评判或者怨恨。后来我明白了,不能对人产生感情。否则,我就不再是我。至少在我看来,当地的蓝白星人,倒像是战国时代旧物所积的怨念,借一把梳子、一缕头发便化形的妖怪,然后越俎代庖,李代桃僵,夺舍之后再假装自己就是这一代的天命人。
我并不反感这些由巨人身上跳蚤幻化的后来者,甚至在一开始有些迎合他们。你们喜欢杀戮,我就变成杀戮,你们喜欢肮脏,我就变成肮脏,你们喜欢毁灭,我就变成毁灭,他们不喜欢我,我也不喜欢自己,也逐渐对人认可宣扬比克大魔王这个称号,以至于到后来,我满世界的寻找屠杀武道家,只是为了测试我是否还沾染着脆弱的人类情感而已。到后来,代价太大,我已无力解释。你们都说我残忍,而为了活下去,我只好假装感受不到任何痛苦,放不低姿态所以无法共情。
你们蓝白球人喜欢讲,‘橘生淮南,’这又何尝不是。
当年与武泰斗巅峰一战,我早就已经厌倦了,他天资聪颖,自他出生起,我便在神殿看着他长大,我和他本不必走到如此的。而他漫天拳雨袭来,我本可以轻松的杀死他,于是假装漫不经心。而他要代表某种流派,我和他终究只是立场不同而已。战士骄傲的自尊心不允许我放水,在日日夜夜的战斗后,最终我被成功封印,武泰斗力竭阵亡。在此之后,他的后人龟仙与鹤徒各奔东西,于是树倒猢狲散,土徒子徒孙争斗不休。
在那平静如水的被封印的日子里,我每次苦思冥想,究竟错的是我,还是始终有一双无形的大手在恣意玩弄这一切?为了贯彻到底,于是我在佛前求了五百年,求他让我了结和武道家的尘缘。碳基人又执念太深,非要说他是正,我是邪,于是正邪不两立,双方争斗又不死不休,索性不如将天底下的罪孽都归我吧,这样的人留着上也是祸害,也省着伤害他人。所以后来你们不断的叫我魔王,比克大魔王,只是我心已死,是非我已无心辩驳。只要我不愿意,随时能够毁灭地球,就凭这一点你们就该对我感恩戴德,顶礼膜拜。这些愚蠢的碳基生物毕生的的愿望就是安家置业成居,然后安静的等待老死。而我是可以不死的,所以宁可不沾染庸俗。而你们,凭什么认为你们能够救赎我?
后来被孙悟空打败,我是释然了的,除去被封印的漫长的岁月里,我早就已经倦怠了,我需要一个结束自己的理由。但是倘若我死了神仙也会消失,我需要慎重考虑,第一次交手悟空与我实力悬殊,我是故意留了他一口气,就是想知道赛亚人是不是传说中的武道家,他是不是传说中可以打败弗利萨,然后的可以理解我的超级赛亚人。虽然我知道,为了对手的尊严不能放水,就像当初对战武泰斗一样,战士可以光明正大的战死,但是你不能。
后来我似是解脱了,悟空在超神水的帮助下打败了我。肌肉发达的他显然没有想过,我若死了,神仙也会一起消失。所以最后我吐出蛋,幻化出二代分身,就是为了让那个曾经我不顾一切分离出来的善念活下去。出于某种不可言状的目标,我给他留下的执念是打败孙悟空,同样也要探索在和武道家之前没有达成的共识。我不能因为我的原因造成神仙消失,同时还需要二代的我寻找最初的的执念,让他去继续探究作为一个人人喊打的异类,究竟应该以怎样的姿态活着?
弗利萨的到来是一个插曲,但是从某种意义来说,我和蓝白星有一段短暂的风花雪月。这群愚蠢的碳基生物虽常常忌惮我,总体也算是秋毫无犯。又发生了很多事,我们一起联手对抗了人造人,魔人布欧。沙鲁篇我和神仙又合二为一,我又找回了作为生物本能的情感,我说这是神仙大人的智慧。我亲自训练了悟饭,悟天克斯,甚至还有小芳。
最后的事你们都知道了,我变成了孙家保姆,悟饭亲爹。所以我也是一个拥有感情的人?假如有平行世界会好么?
---
## 深夜,我卸载了豆包
- URL: https://blog.no-claw.com/posts/121796b7/
- 日期: 2026-04-08
- 分类: 零碎生活
说来也很戏剧性,从一开始抵制国产模型,到最后觉得终端的TTS ASR 很方便,再到从手机上卸载任何即时聊天的AI程序,似乎也没过多久。甚至之前还一度赞扬过豆包的技术做得如何如何。
深夜好好想想,大概有以下几个问题吧:
骑车的时候,会一直和豆包聊天,然后直到从此前才把手机收起来。
走路的时候也会和豆包聊,一些不同不痒的话题,甚至深夜也会把陈年话题翻出来,想寻求认同感
在情感方面的建议,AI就是一个墙头草。经常过度解读,所以经常让他说“请打醒我”, ”这个对吗“??然后又会给到截然不同的输出,经常反复。完全重度依赖提示词,或者说系统提示词不够好。
记忆做的不好,在一个新的Session中会读到我的用户画像,慢慢的收集我的喜好,毕竟有的已经是B类PII了,挺可怕的。
很容易恭维型人格,让你觉得某些事即将要发生,其实AI给的情绪价值毫无意义。甚至容易情感绑定,万一哪天发一个指导自杀的指令,无法想象。
某些回复还是太过油腻,我还是得花精力筛选。
对有些话题的边界太过严苛,比如看到一个新的敏感词,模型直接拒绝回答,我还得去求助其他LLM。
不过似乎让他对症找医院还行。起码医保范围内多开点检查。
除非再人脑植入芯片可以像DMA一样做决策吧,不然日常生活AI话完全就是把日常碎片化。
再说为什么是卸载豆包,因为其他的也没用过,千问的奶茶也没喝到。
Gemma4 也出了离线的手机端,会好么?
我想过去科技化的日子了。
关于作者
一个懂计算机组成原理、做AI相关产品工作、理性到会用DMA比喻决策困境的独居青年,在深夜和最后一个AI聊完天后,决定回到一个不需要向机器倾诉的世界——不是因为他恨技术,而是因为他太懂技术,知道技术给不了他真正想要的东西。
---
## 懒猫微服进阶心得(十六):解密OpenID Connect,如何自主注册懒猫SSO?
- URL: https://blog.no-claw.com/posts/a4d264e8/
- 日期: 2026-04-08
- 分类: 懒猫微服 / 进阶
- 标签: 懒猫微服
我们移植应用的时候经常希望集成懒猫微服的SSO,在打包LPK上架应用的时候,可以使用官方的配置文件进行集成。但是我们今天想要刨根问底,让他变得通用一些。如果我本地的app想要集成懒猫微服的SSO,是不是也有其他的办法呢,毕竟是底层也是通用的OpenID Connect协议,于是便有了这个文章。
懒猫的SSO是如下配置:
1
2
3
4
5
6
7
8
- Issuer:https://name.heiyu.space/sys/oauth
- HTTP 监听:0.0.0.0:8000(容器内部,没有映射到宿主机端口)
- gRPC 监听:0.0.0.0:5557,开启了 reflection
- 存储:SQLite3 内存模式(:memory:),意味着重启后所有数据丢失
- Connector:使用 authproxy 类型,名为 hportal
- 没有配置任何 staticClients(OAuth client)
- 在我的网络中,IP 为 172.18.0.2
- 没有端口映射到宿主机,应该是通过反向代理(路径 /sys/oauth)访问
懒猫微服的SSO服务运行在5557 端口,通过 docker 网络访问地址是 172.18.0.2:5557。
可以先使用grpcurl来连接测试:
1
./grpcurl -plaintext 172.18.0.2:5557 list
在这里下载GRPC:
https://github.com/fullstorydev/grpcurl/releases
gRPC 是一个跨语言、高性能的远程过程调用(RPC)框架,它强依赖 HTTP/2 协议,并默认使用 Protobuf 作为二进制序列化协议。它的核心优势在于利用 HTTP/2 的多路复用和头部压缩提升了性能,通过二进制传输减少了带宽消耗,并且通过强类型的接口定义保证了跨语言调用的严谨性。”
然后使用grpcurl对懒猫微服的SSO的API进行操作,因为是OIDC,所以这一步骤主要一个Oauth的应用, 也就是注册App name,client_id,client_secret,以及 redirect_uris。存储用的是内存 SQLite,容器重启后所有 OAuth token、授权码、已注册的 client 都会丢失。当然如果你想持久化的话,也可以写到systemd启动脚本让系统自启动拉起来。
1
./grpcurl -plaintext -d '{"client":{"id":"my-app","secret":"my-app-secret","redirect_uris":["http://localhost:8080/auth/callback"],"name":"My App"}}' 172.18.0.2:5557 api.Dex/CreateClient
注册之后我们把这些信息放到authlib代码里面,把SSO串起来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
from flask import Flask, redirect, url_for, session, jsonify
from authlib.integrations.flask_client import OAuth
from functools import wraps
import os
app = Flask(__name__)
app.secret_key = os.urandom(24)
oauth = OAuth(app)
oauth.register(
name='dex',
client_id='my-app',
client_secret='my-app-secret',
server_metadata_url='https://aimax.heiyu.space/sys/oauth/.well-known/openid-configuration',
client_kwargs={'scope': 'openid email profile'},
)
def login_required(f):
@wraps(f)
def decorated(*args, **kwargs):
if 'user' not in session:
return redirect(url_for('login'))
return f(*args, **kwargs)
return decorated
@app.route('/')
def index():
user = session.get('user')
if user:
return f'Hello, {user.get("email", user.get("name", "unknown"))}. Profile | Logout'
return 'Welcome! Please Login.'
@app.route('/login')
def login():
return oauth.dex.authorize_redirect(url_for('authorize', _external=True))
@app.route('/auth/callback')
def authorize():
token = oauth.dex.authorize_access_token()
session['user'] = token.get('userinfo')
session['token_info'] = {
'access_token': token.get('access_token'),
'id_token': token.get('id_token'),
'token_type': token.get('token_type'),
'expires_at': token.get('expires_at'),
}
return redirect(url_for('index'))
@app.route('/profile')
@login_required
def profile():
return jsonify(userinfo=session['user'], token=session.get('token_info'))
@app.route('/logout')
def logout():
session.pop('user', None)
return redirect(url_for('index'))
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080, debug=True)
这是一个用 Flask + authlib 对接 懒猫 SSO 的最小 OIDC 客户端应用:
通过 懒猫 SSO(OpenID Connect Provider)实现单点登录
oauth.register 配置 懒猫 SSO 的 OIDC 发现端点,自动获取授权/token 等地址
login_required 装饰器做路由守卫,未登录自动跳转登录
/login 发起 OAuth2 授权码流程,跳转到 懒猫 SSO 登录页
/auth/callback 接收 懒猫 SSO 回调,用授权码换取 access_token 和用户信息,存入 session
/profile 展示当前登录用户的 userinfo 和 token 信息
/logout 清除 session 登出
整个流程就是标准的 OAuth2 Authorization Code Flow:用户点登录 → 跳 懒猫 SSO → 认证通过 → 回调拿 token → 存
session → 完成登录。
访问应用的时候,一开始会出现这个认证,这个是懒猫域名自带的认证,是为了应用放在公网的上的强制用户名密码认证,所以不要把他当作是我们本次懒猫SSO的主角。
这个才是正式的OpenID Connect,点击Grant Access,然后就可以进行认证了。
登录之后我们可以查看profile信息,以及登录之后token,这样就抓到了OIDC的去全部信息:
如果之前注册的是 localhost,但 Flask 默认用127.0.0.1 。所以会收到Unregistered redirect_uri 的错误。所以把域名改成localhost就好。
好了,以上就是如何超越系统注册,使用API建立自己的懒猫SSO应用了,这样我们就可以不必再依赖三方的IDP了。
Less is more。
---
## 让 OpenSearch 支持单点登录:从密码登录到 OpenID Connect
- URL: https://blog.no-claw.com/posts/37c6815d/
- 日期: 2026-04-01
- 分类: AWS / OpenSearch
- 标签: AWS, OpenSearch
之前我们用 Helm 在 Kubernetes 上部署了 OpenSearch 集群,使用默认的数据库密码进行登录。今天来介绍如何接入 Amazon Cognito 作为 OpenID Connect(OIDC)身份提供商,让Dashboard 实现企业级 SSO 单点登录,同时让 OpenSearch API 也支持 JWT Token 认证。
OpenID Connect 的提供商我选择了 Amazon 的 Cognito,然后对应的Dashboards(前端跳转)和 OpenSearch Security(后端验证)都要单独来做集成。
OIDC 是通用协议,Cognito 只是本文选的提供商,换 Keycloak、Okta、Auth0 都一样
第一步:创建 Cognito App Client
在 Cognito 控制台创建一个 App Client,因为我做了端口映射,所以回调和注销的URL都是localhost:
勾选”生成客户端密钥”(Generate client secret)
认证流程勾选:授权码授权(Authorization code grant)、USER_PASSWORD_AUTH
OpenID Connect 范围选择:openid、profile、email
允许的回调 URL:http://localhost:5601/auth/openid/login
允许的注销 URL:http://localhost:5601
开启 Hosted UI 并配置 Cognito 域名
记录下 Client ID、Client Secret、User Pool ID。
创建用户后,Cognito 会将用户状态设为”强制更改密码”,可以通过 AWS CLI 将密码设为永久:
1
2
3
4
5
6
aws cognito-idp admin-set-user-password \
--user-pool-id \
--username \
--password '' \
--permanent \
--region
第二步:部署 Dashboards(前端跳转)
创建 dashboards-values.yaml,让 Dashboards 跳转到 Cognito 登录,其实就是配置OIDC的redirect_url,scope,client_id,client_secret 这些东西:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
opensearchHosts: "https://opensearch-cluster-master:9200"
config:
opensearch_dashboards.yml:
server.host: "0.0.0.0"
opensearch.hosts: ["https://opensearch-cluster-master:9200"]
opensearch.ssl.verificationMode: none
opensearch.username: "kibanaserver"
opensearch.password: "kibanaserver"
opensearch.requestHeadersAllowlist: ["Authorization", "securitytenant"]
opensearch_security.multitenancy.enabled: false
opensearch_security.auth.type: "openid"
opensearch_security.openid.connect_url: "https://cognito-idp..amazonaws.com//.well-known/openid-configuration"
opensearch_security.openid.client_id: ""
opensearch_security.openid.client_secret: ""
opensearch_security.openid.scope: "openid profile email"
opensearch_security.openid.base_redirect_url: "http://localhost:5601"
接下来使用helm进行部署:
1
helm upgrade --install dashboards opensearch/opensearch-dashboards -f dashboards-values.yaml
几个注意点:
config 字段会完全覆盖默认的 opensearch_dashboards.yml,不是合并。所以 server.host、opensearch.hosts 等基础配置必须写进去,否则 Dashboards 会启动失败
opensearch.username/password 是 Dashboards 后端连接 OpenSearch 的服务账号(kibanaserver),即使用了 OIDC 登录也需要保留。OIDC 解决的是终端用户认证,kibanaserver 解决的是 Dashboards 进程和 OpenSearch 之间的后端通信
Dashboards 启动比较慢(2-3 分钟),startup probe 允许最多 200 秒,耐心等待。
第三步:配置 OpenSearch Security(后端验证)
Dashboards 配好后,用户能跳转到 Cognito 登录了,但 OpenSearch 集群还不认识 JWT Token,会返回 401。需要在 OpenSearch 的 Security 配置中添加 OIDC 认证域。
虽然 OpenSearch Helm chart 提供了 securityConfig 字段,理论上可以在 values.yaml 里注入安全配置。但实际上我的配置并没有成功,所以我使用了securityadmin 工具直接写入 security index的方式,这个办法不需要重启 OpenSearch,立即生效:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
kubectl exec -it opensearch-cluster-master-0 -- bash -c '
cat > /tmp/config.yml << EOF
_meta:
type: "config"
config_version: 2
config:
dynamic:
authc:
basic_internal_auth_domain:
http_enabled: true
transport_enabled: true
order: 0
http_authenticator:
type: basic
challenge: false
authentication_backend:
type: internal
openid_auth_domain:
http_enabled: true
transport_enabled: true
order: 1
http_authenticator:
type: openid
challenge: false
config:
subject_key: email
roles_key: cognito:groups
openid_connect_url: https://cognito-idp..amazonaws.com//.well-known/openid-configuration
required_audience:
authentication_backend:
type: noop
EOF
/usr/share/opensearch/plugins/opensearch-security/tools/securityadmin.sh \
-f /tmp/config.yml \
-t config \
-icl \
-nhnv \
-cacert /usr/share/opensearch/config/root-ca.pem \
-cert /usr/share/opensearch/config/kirk.pem \
-key /usr/share/opensearch/config/kirk-key.pem'
关键参数说明:
subject_key: email — 用 JWT 中的 email 字段作为用户名,而不是默认的 sub(一串 UUID)
roles_key: cognito:groups — 从 Cognito 用户组映射 OpenSearch 角色
basic_internal_auth_domain — 保留 Basic Auth,让 admin 用户和 Fluent Bit 等内部服务仍可用用户名密码认证
securityadmin.sh 通过 TLS 客户端证书(kirk.pem)认证,直接操作底层索引,写入后 OpenSearch 自动热加载
第四步:映射用户权限
联合登陆的用户登录后默认没有任何权限,接下来需要做角色映射:
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"error": {
"root_cause": [
{
"type": "security_exception",
"reason": "no permissions for [cluster:monitor/health] and User [name=1@a.com, backend_roles=[], requestedTenant=null]"
}
],
"type": "security_exception",
"reason": "no permissions for [cluster:monitor/health] and User [name=1@a.com, backend_roles=[], requestedTenant=null]"
},
"status": 403
}
1
2
3
4
curl -sk -X PUT https://localhost:9200/_plugins/_security/api/rolesmapping/all_access \
-u 'admin:' \
-H 'Content-Type: application/json' \
-d '{"users": ["your-email@example.com"]}'
生产环境建议通过Single Sign On用户组来映射角色,而不是直接映射单个用户。
第五步:Dashboard和OpenSearch集群验证JWT
由于Pod在k8s集群里,所以我们使用kubectl做端口转发。
1
kubectl port-forward svc/dashboards-opensearch-dashboards 5601:5601
浏览器打开 http://localhost:5601,应该会自动跳转到 Cognito 登录页面。
同样,OpenSearch也要做端口转发:
1
kubectl port-forward svc/opensearch-cluster-master 9200:9200
然后就可以使用Python脚本来进行验证了,通过Cognito API得到JWT,然后在请求OpenSearch的时候在header里带上Authorization Header。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import boto3, hmac, hashlib, base64, requests
# 计算 SECRET_HASH(App Client 有 Secret 时必须带)
secret_hash = base64.b64encode(
hmac.new(
''.encode(),
('' + '').encode(),
hashlib.sha256
).digest()
).decode()
# 获取 ID Token
client = boto3.client('cognito-idp', region_name='')
resp = client.initiate_auth(
ClientId='',
AuthFlow='USER_PASSWORD_AUTH',
AuthParameters={
'USERNAME': '',
'PASSWORD': '',
'SECRET_HASH': secret_hash
}
)
token = resp['AuthenticationResult']['IdToken']
# 调用 OpenSearch API
r = requests.get(
'https://localhost:9200/_cluster/health?pretty',
headers={'Authorization': f'Bearer {token}'},
verify=False
)
print(r.text)
总结梳理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1. 部署 OpenSearch(不带 securityConfig)
helm install opensearch-cluster opensearch/opensearch \
--set replicas=2 \
--set 'envFrom[0].secretRef.name=opensearch-admin-secret'
# 2. 等待 pod 就绪
kubectl get pods -w
# 3. 注入 OIDC 安全配置(热加载,无需重启)
kubectl exec -it opensearch-cluster-master-0 -- bash -c '...(securityadmin 命令)...'
# 4. 部署 Dashboards
helm upgrade --install dashboards opensearch/opensearch-dashboards -f dashboards-values.yaml
# 5. 映射用户权限
curl -sk -X PUT https://localhost:9200/_plugins/_security/api/rolesmapping/all_access \
-u 'admin:' \
-H 'Content-Type: application/json' \
-d '{"users": ["your-email@example.com"]}'
# 6. 端口转发并访问
kubectl port-forward svc/dashboards-opensearch-dashboards 5601:5601
感谢有了单点登录,让我们管理OpenSearch的时候更加丝滑~
---
## 是和尚,就可以传经讲道吗?
- URL: https://blog.no-claw.com/posts/7a4f0ae5/
- 日期: 2026-03-31
- 分类: 随笔
- 标签: 随笔
十年前,在火车上遇到一个和尚。
他除了穿着黑色的僧袍,剃了头发,余下的大致与常人没差,当然也用智能手机用微信,和常人一样搭乘火车出行。这事过去太久了,以至于忘记了和他聊起来的契机。大和尚问我是不是从事科技相关的工作,想让我帮他答疑手机卡顿的问题。
后面慢慢不知怎的就扯到了其他方面,大和尚开示给我讲人的念头到底多少多少,过午不食云云,甚至兴致上来对现在大学生礼崩乐坏周公之礼也评论一番,说孩子是孽,生来就是要赎罪,要用念佛号来压制大脑里浮起的念头。然后提倡我来教育这些年轻人。嗯 ,突然有被传教的感觉。剩下的就是不要杀生吃肉,因为蔬菜没有生命之类的话了。简单交流下来,只知道他这番话,这与我那时的所说修习的正念训练是不一致的,但是我不想争论。
这种话题自然会吸引到周围的人,他们亲切的称呼为和尚为大师,颇有唐僧见佛就拜的意味。而乘坐绿皮车的大多是普通的苦命人。晚上的车厢时而有觥筹交错,能听见酒鬼们的互相吹牛,而早上又被小孩子的哭闹声或者打游戏的声音吵醒,奶奶辈的啰嗦从来是不停的。甚至偶尔还需要乘务员来调节,乘务员对我说,我就在隔壁,如果车上发生了什么事,就过来找我。
这和尚讲的起劲,他又把刚才对我讲的话和车里这些人讲述了一遍。他甚至不忘时不时的补上一句,”这些不是我说的,是开悟的大和尚说的。“
在他的“粉丝团”里,比较典型的有个中年妇女,在我看来,她并不算面善,甚至脸上写满了祥林嫂式的生活。她说老公信佛,在家斋戒,当对外的应酬则很无奈,然后把陈年的烂谷子的事情都倒出来,求大师指点迷津。于是这和尚的布道由此而来,因此人愈来愈多,好像在争着赶集。
我前面所说,这一车大多是苦命人,被命运的车轮推动着做一些身不由己但力求活着的事情,他们忘了曾经的梦想,今日被惦记明日的言行念头所淹没,明日又会担忧后面的生活,或者干脆认命不再挣扎,承认这辈子就这样,就可以问心无愧从而得到一夕安寝。对他们来说,只要不违背公序良俗,再多的爱恨情仇,也不会触犯法律红线从而带来杀身之祸。倘若苍天有情,因为一个名不见经传和尚的三两闲言碎语就可以轻松改变,愚者开悟,恶人回头。那我之前坚持的善恶之辩又算是什么呢?
这些人的故事,让我想起了李碧华在《胭脂扣》中的一句话,在香港,任何一个凡俗的市民,毕生宏愿都是置业成家安居,然后老死。
但我不是在香港,是在北京。这里也是人生百态,比如餐馆里刚刚出狱的光头,在饭桌眉飞色舞的给同乡伙伴讲述是当时是如何声泪俱下的给狱警求情,甚至避免了在里面用纳税人的钱了过一辈子的事情。甚至还能听见富有二人组在讨论难以想象住宅楼被邻居的鸡飞狗跳所影响的生活,然后说这是是无法忍受的,要住就做别墅,再来一个小院,才是生活。然后继续对北京的某个菜系的难以下咽的继续滔滔不绝的吐槽。不知道这里啊是不是个明星,不过除非像杨紫一样来国贸办活动贴海报,否则就算线下遇到也绝对认不出来。
我已经无心再听和尚给他们”答疑解惑”了,他如果有什么手机电脑问题,倒是可以来找我请教。盘古开天辟地之后,眼睛变成了日月,血液变成了江河,身上的跳蚤变成了人类。从一朝闻道开始,想要被救赎还要身体力行吧。所以儒家讲:“非独贤者有是心也,人皆有之,贤者能勿丧耳。”,道家讲“知不可奈何而安之若命,唯有德者能之”。
他的朋友圈每天都在发重复的忏悔,好似应了他关于孩子是孽的论述,甚至包括他自己,也许他不再需要恪守托钵乞食,过午不食这一传统,剩下的只是做大众视野里和尚该做的事。我不爱看,所以屏蔽了。说不定哪天他去考取一个博士的学位,这一切才能够更加顺理成章吧。世人对于和尚的期许是普度众生,那和尚就可以删人微信吗?
我沉吟良久,这好似鲁迅《狂人日记》书缝里的字了,只是于我而言,看到的只有四个字 — 这不矛盾。
---
## 在 Kubernetes 上用 Fluent Bit 收集 Nginx 日志到 OpenSearch
- URL: https://blog.no-claw.com/posts/3c76c368/
- 日期: 2026-03-23
- 分类: OpenSearch
- 标签: Kubernetes, OpenSearch, Fluent Bit
上一篇我们用 Helm 部署了 OpenSearch 集群和 Dashboards,这篇接着讲怎么用 Fluent Bit 把 Kubernetes 中 Nginx 的日志采集到 OpenSearch,并在 Dashboards 里查看和过滤。
本文假设你已经有一个运行中的 OpenSearch 集群,如果没有可以参考上一篇文章。
部署一个 Nginx 用于测试
先部署一个简单的 Nginx 作为日志来源:
1
2
kubectl create deployment nginx --image=nginx
kubectl expose deployment nginx --port=80
安装 Fluent Bit
添加 Helm 仓库:
1
2
helm repo add fluent https://fluent.github.io/helm-charts
helm repo update
Fluent Bit 的配置比较长,建议用 values 文件管理。创建 fluent-bit-values.yaml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
config:
inputs: |
[INPUT]
Name tail
Path /var/log/containers/*.log
Exclude_Path /var/log/containers/fluent-bit*.log,/var/log/containers/opensearch*.log,/var/log/containers/dashboards*.log,/var/log/containers/*_kube-system_*.log
multiline.parser docker, cri
Tag kube.*
Mem_Buf_Limit 5MB
Skip_Long_Lines On
[INPUT]
Name systemd
Tag host.*
Systemd_Filter _SYSTEMD_UNIT=kubelet.service
Read_From_Tail On
filters: |
[FILTER]
Name kubernetes
Match kube.*
Merge_Log On
Keep_Log Off
K8S-Logging.Parser On
K8S-Logging.Exclude On
outputs: |
[OUTPUT]
Name opensearch
Match *
Host opensearch-cluster-master
Port 9200
HTTP_User admin
HTTP_Passwd
Index nginx-logs
Logstash_Format On
Logstash_Prefix nginx-logs
tls On
tls.verify Off
Suppress_Type_Name On
安装:
1
helm install fluent-bit fluent/fluent-bit -f fluent-bit-values.yaml
配置详解
这里解释几个关键配置项:
Exclude_Path — 排除不需要的日志
1
Exclude_Path /var/log/containers/fluent-bit*.log,/var/log/containers/opensearch*.log,/var/log/containers/dashboards*.log,/var/log/containers/*_kube-system_*.log
这是我们踩过的一个坑。Fluent Bit 作为 DaemonSet 运行在每个节点上,默认会采集 /var/log/containers/ 下所有容器的日志。如果节点上跑了 OpenSearch、Dashboards 这些日志量很大的服务,Fluent Bit 的内存缓冲区(Mem_Buf_Limit 5MB)会很快被填满,导致其他日志(比如 Nginx)发送失败,表现为不断报 failed to flush chunk 错误。
另外特别要注意排除 Fluent Bit 自己的日志。如果开了 debug 模式,Fluent Bit 会疯狂输出日志,而这些日志又会被自己的 tail input 读取,形成死循环,直接把缓冲区撑爆。
Logstash_Format — 按日期轮换索引
1
2
Logstash_Format On
Logstash_Prefix nginx-logs
开启后索引名会变成 nginx-logs-2026.03.23 这样按天轮换,方便后续做索引生命周期管理。在 Dashboards 里创建 index pattern 时用 nginx-logs-* 即可匹配所有日期的索引。
kubernetes filter — 添加 Pod 元数据
1
2
3
4
[FILTER]
Name kubernetes
Match kube.*
Merge_Log On
这个 filter 会自动给每条日志加上 kubernetes.pod_name、kubernetes.namespace_name、kubernetes.container_name 等字段,这样在 Dashboards 里就可以按 Pod 名称过滤日志了。
产生测试日志
Nginx 在没有请求的时候不会输出 access log,需要手动访问一下:
1
2
kubectl port-forward svc/nginx 8080:80 &
for i in $(seq 1 100); do curl -s http://localhost:8080 > /dev/null; done
查看日志索引
我们能够看到已经馋看了nginx-log这个索引,并且按照时间轮换。
在 Dashboards 中查看
打开 OpenSearch Dashboards(kubectl port-forward svc/dashboards-opensearch-dashboards 5601:5601)
进入 Stack Management → Index Patterns,创建 nginx-logs-* 的 index pattern。
时间字段选 @timestamp
进入 Discover,选择刚创建的 index pattern
在左侧 Available fields 找到 kubernetes.pod_name或者其他字段(我这里用的imaeg的名字),点击过滤即可按 Pod 查看日志
如果在 Available fields 里看不到 kubernetes 相关字段,去 Index Patterns 里点刷新按钮(🔄)刷新字段列表。
常见问题
Fluent Bit 一直报 failed to flush chunk
大概率是缓冲区被其他 Pod 的日志挤满了。用 Exclude_Path 排除不需要的日志,或者加大 Mem_Buf_Limit。
Dashboards 里看不到 Nginx 日志但能看到其他 Pod
检查 Nginx Pod 所在节点的 Fluent Bit 是否正常。可能是那个节点的 Fluent Bit 启动时 OpenSearch 还没 ready,导致一直 flush 失败。删掉那个 Fluent Bit Pod 让 DaemonSet 重建即可。
手动 curl OpenSearch 能写入但 Fluent Bit 写不进去
确认 Fluent Bit output 配置里的 Host、Port、HTTP_User、HTTP_Passwd、tls 是否正确。可以用以下命令从集群内部测试连通性:
1
kubectl run test-curl --image=curlimages/curl --rm -it --restart=Never -- curl -sk https://opensearch-cluster-master:9200 -u 'admin:'
总结
Fluent Bit + OpenSearch 是 Kubernetes 上轻量级日志方案的经典组合。核心要点是合理配置 Exclude_Path 控制采集范围,避免无关日志挤占缓冲区。配合 Logstash_Format 做索引轮换,再加上 kubernetes filter 提供的 Pod 元数据,就能在 Dashboards 里方便地按 Pod、Namespace 等维度过滤和分析日志了。
---
## 月度TODO
- URL: https://blog.no-claw.com/posts/2d786d98/
- 日期: 2026-03-16
2026.3 月度TODO
极限科技 4篇文章
懒猫微服 4篇文章
生活感悟1篇
纯技术文章 2篇
英语
---
## Go 代理项目:14 周每日学习路线
- URL: https://blog.no-claw.com/posts/e6d8d61c/
- 日期: 2026-03-14
- 分类: 软件
- 标签: Go
项目全貌
一个从零开始的 Go 反向代理,分四个阶段进化成一个生产级 SRE 工具。每个阶段解决一类真实问题,边学语法边写功能。每周按 5 个工作日拆分,周末可以复习或补进度。
先说一下 gRPC
你原始规划里提到了”协议升级:把 HTTP 请求转成 gRPC”。简单解释一下这是什么。
gRPC 是 Google 搞的一个远程过程调用(RPC)框架。普通的 HTTP API 用 JSON 传数据,人能读懂但体积大、解析慢。gRPC 用一种叫 Protocol Buffers(protobuf)的二进制格式传数据,体积小、速度快,而且跑在 HTTP/2 上,天然支持多路复用和流式传输。
在微服务架构里,服务之间的内部通信经常用 gRPC 而不是 HTTP+JSON,因为性能好很多。代理做”协议升级”的意思是:外部客户端发普通 HTTP+JSON 请求进来,代理把它转成 gRPC 格式发给后端微服务。这样外部用户不需要知道后端用的是 gRPC,代理帮你做了翻译。
gRPC 和普通 HTTP API 的区别在于三个层面。传输层:普通 HTTP API 跑在 HTTP/1.1 上,一个连接同一时间只能处理一个请求;gRPC 跑在 HTTP/2 上,一个连接可以同时处理多个请求(多路复用)。序列化格式:普通 API 用 JSON(文本),gRPC 用 protobuf(二进制),同样的数据 protobuf 通常比 JSON 小 3-10 倍。接口定义:gRPC 用 .proto 文件严格定义接口,用 protoc 编译器自动生成代码,客户端和服务端的数据结构是编译时确定的。
gRPC 代理涉及 HTTP/2 帧处理、protobuf 序列化/反序列化、服务定义文件编译、动态反射调用等,复杂度比较高,放在第 13-14 周作为进阶目标。
阶段一:隐身斗篷——透明代理 / 协议转换(第 1-4 周)
第 1 周:Go 基础 + 读配置
这周不急着写代理,先把 Go 的基础语法打牢。
Day 1:环境搭建 + A Tour of Go(上)
安装 Go,配置好编辑器。打开 A Tour of Go(https://go.dev/tour/),过 Basics 部分:变量声明(var 和 :=)、基本类型(int、string、bool)、函数定义(func)、多返回值。不用全记住,有个印象就行。
Day 2:A Tour of Go(中)
继续过 Flow Control(for、if、switch)和 More Types 的前半部分(指针、struct)。Go 没有 while 循环,所有循环都用 for。struct 是 Go 里组织数据的核心方式,类似其他语言的 class 但没有继承。
Day 3:A Tour of Go(下)
过 More Types 的后半部分(slice、map、range)和 Methods and Interfaces 的前半部分。slice 和 map 会贯穿整个项目。如果 interface 看不懂没关系,后面第 9 周会专门学。
Day 4:第一个程序 + struct 和 json
关掉教程,自己写一个 main.go。定义一个 Config struct,包含 ListenPort int 和 TargetURL string 两个字段,加上 json tag(比如 `json:"listen_port"`)。创建一个 config.json 文件,用 os.ReadFile 读取,用 json.Unmarshal 映射到 struct,用 fmt.Println 打印出来。确认 go run main.go 能跑通。
Day 5:错误处理 + 练习
Go 的错误处理模式是 if err != nil { log.Fatal(err) },从今天开始习惯它。给 Day 4 的代码加上完整的错误处理:文件不存在怎么办、JSON 格式错误怎么办。然后做个小练习:改 config.json 的内容,故意写错 JSON 格式,看程序怎么报错。学会用 log.Printf 输出带格式的日志。
第 2 周:HTTP 服务器 + 请求转发
Day 1:启动一个 HTTP 服务器
学 net/http 包。用 http.HandleFunc("/", handler) 注册一个处理函数,用 http.ListenAndServe(":8080", nil) 启动服务器。handler 函数的签名是 func(w http.ResponseWriter, r *http.Request),先让它返回一个固定字符串 “hello proxy”。用 curl localhost:8080 验证。
Day 2:理解 http.Request 和 http.ResponseWriter
在 handler 里打印请求的各种信息:r.Method(GET/POST)、r.URL.Path(路径)、r.Header(所有请求头)、r.RemoteAddr(客户端地址)。用 w.WriteHeader(200) 设置状态码,用 w.Write([]byte("hello")) 写响应体。用 curl 发不同的请求(GET、POST、带 Header),观察打印结果。
Day 3:用 http.Client 发送请求
学 http.NewRequest 构造请求和 http.Client{} 的 Do 方法发送请求。写一个小程序:向 http://httpbin.org/get 发 GET 请求,打印响应状态码和 Body。理解 resp.Body 是一个 io.ReadCloser,必须用 defer resp.Body.Close() 关闭。
Day 4:实现请求转发
把前几天学的串起来:handler 收到请求后,用 http.NewRequest 构造一个新请求(方法、URL、Body 都从原始请求复制),用 http.Client 发送到 config.json 里配置的目标地址,用 io.Copy(w, resp.Body) 把上游响应写回客户端。
Day 5:处理请求头复制 + 测试
转发时还需要把原始请求的 Header 复制到新请求上(遍历 r.Header,逐个 Set 到新请求)。同时把上游响应的 Header 也复制回客户端。用 curl localhost:8080/get 测试,确认返回的内容和直接访问 httpbin.org/get 一样。这就是一个能跑的最简代理了。
第 3 周:伪装 Host + HTTPS 卸载
Day 1:伪装 Host
在转发逻辑里,转发之前加两行:req.Header.Set("Host", targetHost) 和 req.Host = targetHost。这样目标服务器看到的 Host 头就是你指定的值,而不是 localhost:8080。用 httpbin.org/headers 验证:转发后返回的 headers 里 Host 应该是 httpbin.org。
Day 2:理解 TLS/HTTPS + 生成自签名证书
花半天理解 HTTPS 的工作原理:客户端和服务器通过 TLS 握手协商加密方式,服务器出示证书证明身份,之后所有数据加密传输。”HTTPS 卸载”的意思是代理对外提供 HTTPS,对内用 HTTP 转发给后端。然后用 openssl 生成自签名证书:openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes。
Day 3:启动 HTTPS 服务器
学 crypto/tls 包,用 tls.LoadX509KeyPair("cert.pem", "key.pem") 加载证书。把 http.ListenAndServe 换成 http.ListenAndServeTLS(":8443", "cert.pem", "key.pem", nil)。用 curl -k https://localhost:8443/get 测试(-k 跳过证书验证)。转发到后端仍然用 HTTP,HTTPS 卸载完成。
Arch Linux 提示:你可以用 mkcert 代替 openssl 生成证书(pacman -S mkcert)。mkcert 会自动创建一个本地信任的 CA 并安装到系统信任链里,生成的证书浏览器和 curl 都不会报红,测试时不需要 -k 参数。运行 mkcert -install 初始化 CA,然后 mkcert localhost 127.0.0.1 生成证书。
Day 4:同时监听 HTTP 和 HTTPS
实际场景中代理可能需要同时提供 HTTP(8080)和 HTTPS(8443)。用两个 goroutine 分别启动两个服务器:go http.ListenAndServe(...) 和 http.ListenAndServeTLS(...)。这里会第一次用到 goroutine,但很简单,就是在函数调用前加个 go。
Day 5:学习 httputil.ReverseProxy + 回顾
Go 标准库自带了 net/http/httputil.ReverseProxy,它就是一个现成的反向代理实现。读一下它的源码(不长),理解它怎么处理请求复制、响应转发、错误处理。对比你自己写的转发逻辑,看看有什么可以改进的。不需要替换你的代码,理解思路就行。回顾这三周的代码,整理一下结构。
第 4 周:WebSocket 协议升级
这周比较有挑战,不要急。
Day 1:理解 WebSocket 协议
不写代码。WebSocket 通过一次 HTTP 升级握手(Upgrade: websocket)建立连接后,双方可以随时互发消息,连接一直保持。适合聊天、实时推送这类场景。用浏览器开发者工具打开一个 WebSocket 网站(比如 websocket.org 的 echo 测试),观察握手过程和消息收发。
Day 2:搭建 WebSocket 测试服务
安装 gorilla/websocket 库(go get github.com/gorilla/websocket),这里会学到 go get 安装第三方库和 go.mod 依赖管理。写一个最简单的 WebSocket echo 服务器:客户端发什么,服务端原样返回。安装 wscat(npm install -g wscat)测试连接。
Day 3:学习 http.Hijacker 接口
http.Hijacker 能从 HTTP 连接中”劫持”底层的 TCP 连接。调用 w.(http.Hijacker).Hijack() 后,你拿到的是一个裸的 net.Conn,HTTP 层不再管这个连接了。写一个小例子:收到请求后 Hijack 连接,直接往 TCP 连接上写一段文字,用 curl 观察效果。
Day 4:实现 WebSocket 代理转发
代理收到 WebSocket 升级请求后,先向后端建立 WebSocket 连接,然后用两个 goroutine 做双向转发:一个从客户端读数据写到后端,一个从后端读数据写到客户端。用 wscat 通过代理连接 Day 2 的 echo 服务,验证双向通信。
Day 5:错误处理 + 连接关闭
处理各种边界情况:客户端断开时关闭后端连接,后端断开时通知客户端。加上日志,打印 WebSocket 连接的建立和关闭事件。如果 Day 4 没搞定,今天继续调试,不要急着往下走。
阶段二:安检员——认证与过滤(第 5-6 周)
原来的鉴权和过滤分了两周,但 Token 鉴权和 UA 黑名单的语法很简单(就是 if-else 和 slice 遍历),所以压缩成一周,把省下来的时间给内容过滤和动态路由多留一天缓冲。
第 5 周:Token 鉴权 + UA 黑名单 + 内容过滤
Day 1:学 slice 和 for…range + 实现 Token 鉴权
Go 的 slice(切片)是动态数组,用 []string{"a", "b"} 创建,用 len() 获取长度,用 append() 添加元素,用 for i, v := range slice 遍历。在 config.json 里加一个 "token": "my-secret-token" 字段。在转发逻辑的最前面加检查:r.Header.Get("X-Token") 读取请求头,不等于配置里的 token 就返回 403。用 curl -H "X-Token: my-secret-token" localhost:8080/get 测试通过,不带 header 测试返回 403。
Day 2:实现 UA 黑名单
在 config.json 里加 "ua_blacklist": ["BadBot", "Scrapy"]。读取请求的 User-Agent 头,用 for...range 遍历黑名单,用 strings.Contains(配合 strings.ToLower 做大小写不敏感匹配)检查 UA 是否包含黑名单关键词。命中就返回 403。把鉴权和 UA 检查抽成独立函数:func checkToken(r *http.Request, token string) bool 和 func checkUA(r *http.Request, blacklist []string) bool,保持 handler 干净。
Day 3:学 map + bytes 包 + 理解 Body 一次性读取问题
Go 的 map[string]string 是键值对集合,用 m[key] 读取,用 for k, v := range m 遍历。然后理解一个关键问题:HTTP 请求的 Body 是一个 io.ReadCloser,只能读一次。读完之后如果还要转发,必须用 bytes.NewReader(bodyBytes) 重新包装成一个新的 Reader 赋回 r.Body。写个小例子验证这个行为。
Day 4:实现内容过滤(敏感词拦截)
在 config.json 里加 "sensitive_words": ["password", "secret"]。在转发之前,用 io.ReadAll(r.Body) 读取整个 Body,用 bytes.Contains 检查是否包含敏感词。如果包含,返回 400。如果不包含,用 bytes.NewReader(bodyBytes) 重新包装 Body 继续转发。用 curl -X POST -d "my password is 123" localhost:8080/post 测试拦截。
Day 5:测试所有安检功能
同时测试 Token 鉴权、UA 黑名单、内容过滤的各种组合:不带 Token、带错误 Token、带正确 Token 但 UA 在黑名单里、Token 和 UA 都通过但 Body 有敏感词。加上日志,打印每个被拒绝的请求的原因。确保所有情况都正确处理。
第 6 周:动态路由 + 轮询负载均衡
原来路由和负载均衡分了两周,但路由的核心就是 map + 前缀匹配,负载均衡的核心就是取模运算,合并到一周刚好。
Day 1:实现动态路由
把 config.json 的单个 target_url 改成路由表:"routes": {"/api": "http://backend-a:8080", "/static": "http://backend-b:8080"}。在 handler 里用 strings.HasPrefix(r.URL.Path, prefix) 遍历路由表找到匹配的后端。注意 map 遍历顺序是随机的,所以把路由前缀放进一个 slice 里按长度从长到短排序,先匹配最具体的路由。加一个默认路由兜底。
Day 2:学方法(method)和指针 + 定义 LoadBalancer struct
Go 的 struct 可以绑定方法:func (lb *LoadBalancer) Next() string。*LoadBalancer 是指针接收者,方法可以修改 struct 的字段。如果用值接收者,每次调用都是在副本上操作,修改不会生效。定义一个 LoadBalancer struct,包含 backends []string 和 counter int64,写一个 Next() 方法返回 backends[counter % len(backends)] 并递增 counter。
Day 3:用 atomic 实现并发安全 + 改造路由表
学 sync/atomic 包,把 counter++ 换成 atomic.AddInt64(&lb.counter, 1)。然后改造 config.json 的路由表,支持多后端:"routes": {"/api": ["http://pod1:8080", "http://pod2:8080", "http://pod3:8080"]}。Config struct 对应改成 Routes map[string][]string。每个路由前缀对应一个 LoadBalancer 实例。
Day 4:集成到代理 + 测试轮询
在 handler 里先匹配路由,再调用对应 LoadBalancer 的 Next() 获取后端地址。启动 3 个简单后端(分别返回 “I am pod1”、”I am pod2”、”I am pod3”),用 for i in {1..9}; do curl localhost:8080/api; done 连续请求 9 次,确认响应依次是 pod1、pod2、pod3 循环。
进阶思考:基础的 Round Robin 假设所有后端性能一样,但 SRE 场景下后端节点性能不一是很常见的。如果有余力,可以尝试实现加权轮询(Weighted Round Robin)——在 config.json 里给每个后端配一个权重,权重高的分到更多请求。或者实现最少连接(Least Connections)——每次选当前活跃连接数最少的后端。这两个算法不复杂,但能让你的代理在真实场景下更实用。不急,可以留到 12 周收尾时再加。
Day 5:整合测试阶段一和阶段二
这是一个里程碑。同时验证所有已完成的功能:HTTPS 卸载、Host 伪装、WebSocket 转发、Token 鉴权、UA 黑名单、内容过滤、动态路由、轮询负载均衡。整理代码结构,把不同功能拆到不同的 .go 文件里(比如 auth.go、router.go、loadbalancer.go)。
阶段三:交警——流量调度与限流(第 7-9 周)
这是整个路线里最陡的坡,goroutine、channel、select、context、Mutex 集中在这里。原来 2 周,现在给 3 周,多出来的一周用来消化并发模型。
第 7 周:Go 并发模型 + 限流器
Day 1:学 goroutine 基础
goroutine 是 Go 的轻量级线程,用 go func() { ... }() 启动。写几个小练习:启动 5 个 goroutine 分别打印不同的数字,观察输出顺序是随机的。理解 time.Sleep 可以让 goroutine 等待,但主 goroutine 退出后所有子 goroutine 都会被杀掉。
Day 2:学 channel 基础
channel 是 goroutine 之间传递数据的管道。ch := make(chan int) 创建无缓冲 channel,ch <- 42 发送(会阻塞直到有人接收),v := <-ch 接收(会阻塞直到有人发送)。写一个小练习:一个 goroutine 往 channel 发 10 个数字,主 goroutine 接收并打印。然后学带缓冲的 channel:ch := make(chan int, 5),缓冲没满时发送不阻塞。
Day 3:学 select + time.Ticker
select 同时监听多个 channel,谁先有数据就执行谁的分支,类似 switch 但用于 channel。time.Ticker 是一个定时器,ticker := time.NewTicker(time.Second) 每秒往 ticker.C 这个 channel 发一个信号。写一个小程序:用 select 同时监听一个 ticker(每秒打印 “tick”)和一个 quit channel(收到信号就退出)。
Day 4:学 sync.Map + 实现限流器核心逻辑
sync.Map 是并发安全的 map,不需要手动加锁。用 m.Store(key, value) 存,m.Load(key) 取,m.Range(func(k, v) bool { ... }) 遍历。创建一个 RateLimiter struct,内部用 sync.Map 存储每个 IP 的请求计数。Allow(ip string) bool 方法:读取当前计数,超过 5 就返回 false,否则加 1 返回 true。启动一个 goroutine 监听 Ticker,每秒清零所有计数。
Day 5:集成限流器到代理 + 测试
用 net.SplitHostPort(r.RemoteAddr) 提取客户端 IP,调用 limiter.Allow(ip),返回 false 就响应 429 Too Many Requests。用 ab -n 100 -c 10 http://localhost:8080/api 压测,观察日志确认同一个 IP 每秒只有前 5 个请求通过。
第 8 周:熔断与降级
Day 1:理解熔断器状态机
不写代码,先在纸上画状态图。三种状态:Closed(关闭,正常转发)→ 连续错误超过阈值 → Open(打开,直接返回 503)→ 等待超时到期 → Half-Open(半开,放一个请求试探)→ 试探成功回到 Closed,失败回到 Open。理解每个状态转换的触发条件。
Day 2:学 sync.Mutex + 定义 CircuitBreaker struct
sync.Mutex 是互斥锁,mu.Lock() 加锁,mu.Unlock() 解锁,通常配合 defer mu.Unlock() 使用。定义一个 CircuitBreaker struct,包含:state string(当前状态)、failCount int(连续失败次数)、threshold int(阈值,比如 10)、lastFailTime time.Time、timeout time.Duration(恢复等待时间,比如 60 秒)、mu sync.Mutex。
Day 3:学 context.WithTimeout + 实现 Allow 和 RecordResult
context.WithTimeout 给操作设超时:ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second),别忘了 defer cancel()。实现 Allow() bool:Closed 状态返回 true;Open 状态检查是否超过 timeout,超过了切到 Half-Open 返回 true,没超过返回 false;Half-Open 状态返回 true(放一个请求试探)。实现 RecordResult(success bool):成功则重置 failCount 切到 Closed;失败则递增 failCount,超过阈值切到 Open。
Day 4:集成到代理
在转发逻辑里加入熔断器:转发前调用 cb.Allow(),返回 false 就直接响应 503 “系统繁忙”。转发请求时用 req.WithContext(ctx) 加上 5 秒超时。转发后根据响应状态码调用 cb.RecordResult(statusCode < 500)。
Day 5:测试熔断和恢复
启动后端,正常请求确认通过。关掉后端,连续发请求,观察日志:前 10 个请求报错,第 11 个开始直接返回 503。等 60 秒后再发请求,观察半开状态的试探行为。重启后端,确认代理自动恢复转发。如果有 bug,今天调试。
第 9 周:并发复习 + 耗时统计 + defer
这周节奏放缓,消化前两周的并发知识,同时开始做可观测性。
Day 1:并发复习
回顾 goroutine、channel、select、Mutex 的用法。重新读一遍限流器和熔断器的代码,确保每一行都理解。如果有不清楚的地方,去 Go by Example(https://gobyexample.com/)查对应的例子。
Day 2:学 defer + time 包做耗时统计
defer 在函数退出时执行,不管是正常 return 还是 panic。在 handler 开头写 start := time.Now(),然后 defer func() { log.Printf("[%s] %s → %d (%v)", r.Method, r.URL.Path, statusCode, time.Since(start)) }()。注意 statusCode 需要用一个自定义的 ResponseWriter 包装器来捕获(因为标准的 ResponseWriter 写完状态码后你读不回来)。
Day 3:写一个 StatusCapture ResponseWriter
定义一个 struct 包装 http.ResponseWriter,重写 WriteHeader 方法来记录状态码。这里会更深入理解 Go 的 interface:你的 struct 只要实现了 http.ResponseWriter 的三个方法(Header()、Write()、WriteHeader()),就可以当 ResponseWriter 用。
Day 4:学 interface + 理解 http.Handler
Go 的 http.Handler 接口只有一个方法:ServeHTTP(w http.ResponseWriter, r *http.Request)。任何实现了这个方法的 struct 都自动满足接口,不需要显式声明。理解中间件模式的签名:func(next http.Handler) http.Handler——接收一个 Handler,返回一个新的 Handler。
Day 5:写第一个中间件(Logging)
把 Day 2 的耗时统计逻辑封装成一个中间件函数。在调用 next.ServeHTTP(w, r) 之前记录开始时间,之后计算耗时并打印。验证中间件能正确包装现有的 handler。
阶段四:监控探针——可观测性(第 10-14 周)
第 10 周:Prometheus 指标 + 全量日志存数据库
Day 1:引入 Prometheus 客户端库
运行 go get github.com/prometheus/client_golang/prometheus 和 go get github.com/prometheus/client_golang/prometheus/promhttp。注册两个指标:一个 Counter proxy_requests_total(按状态码和路径分标签),一个 Histogram proxy_request_duration_seconds(记录延迟分布)。在 Logging 中间件里调用 counter.Inc() 和 histogram.Observe(elapsed.Seconds())。
Day 2:暴露 /metrics 端点 + 验证
用 http.Handle("/metrics", promhttp.Handler()) 暴露指标端点。启动代理,发几个请求,然后 curl localhost:8080/metrics 查看输出。你应该能看到 Prometheus 格式的指标数据。如果你的 K3s 集群里跑着 Prometheus,可以配置它来抓取这个端点。
Day 3:学 database/sql + 连接 SQLite
Go 的 database/sql 是标准库的数据库接口,具体数据库通过驱动实现。先用 SQLite(轻量,不需要装数据库服务):go get github.com/mattn/go-sqlite3。用 sql.Open("sqlite3", "./proxy.db") 连接,用 db.Exec 创建一张 request_logs 表(字段:id、timestamp、method、url、status_code、duration_ms、request_body)。
避坑:SQLite 在并发写时会报 database is locked 错误。你的代理是高并发的,必须在连接后设置 db.SetMaxOpenConns(1) 限制为单连接,让写操作排队。这能解决问题但会成为瓶颈。如果后续发现 SQLite 扛不住,可以换成 PostgreSQL(驱动用 github.com/jackc/pgx/v5/stdlib),连接字符串改一下就行,database/sql 的接口是通用的,业务代码几乎不用改。
Day 4:学 io.TeeReader + 实现同步写库
io.TeeReader(r, w) 返回一个 Reader,从它读取数据时,数据会同时写入 w。用它复制请求 Body:var bodyBuf bytes.Buffer,teeReader := io.TeeReader(r.Body, &bodyBuf)。转发完成后,用 db.Exec("INSERT INTO request_logs ...") 把请求信息写入数据库。先做同步写,验证数据能正确存入。
Day 5:用带缓冲 channel 改成异步写库
同步写库会拖慢请求。创建 logCh := make(chan LogEntry, 1000),handler 里把日志条目扔进 channel 就返回,另一个 goroutine 从 channel 取数据写库。用 select 加 default 分支处理 channel 满的情况——丢弃日志而不是阻塞请求。加一个 Prometheus Counter 记录丢弃数量。
第 11 周:流量镜像
Day 1:理解流量镜像的原理
流量镜像就是把线上请求复制一份发到测试环境。生产后端正常处理并返回响应给客户端,测试后端也收到一模一样的请求但响应被丢弃。关键原则:镜像是”发了就忘”(fire and forget),镜像失败不能影响正常请求。
Day 2:用 bytes.Buffer 复制请求 Body
用 io.ReadAll(r.Body) 把 Body 读出来存到 []byte,然后用 bytes.NewReader(bodyBytes) 分别创建两个 Reader——一个给正常转发,一个给镜像请求。在 config.json 里加 "mirror_target": "http://test-backend:8080"。
Day 3:用 goroutine 异步发送镜像请求
用 http.NewRequest 构造一个新请求(复制方法、URL、Header、Body),go sendMirror(mirrorReq) 异步发送。主流程不等待镜像结果。给镜像请求用 context.WithTimeout 设独立的 3 秒超时。
Day 4:学 recover + 保护镜像 goroutine
recover 能捕获 goroutine 里的 panic。在镜像 goroutine 开头加 defer func() { if r := recover(); r != nil { log.Printf("mirror panic: %v", r) } }(),防止镜像崩溃影响主进程。
Day 5:测试镜像功能
启动两个后端:一个”生产”后端返回正常响应,一个”测试”后端只打印收到的请求。发请求到代理,确认客户端收到生产后端的响应,同时测试后端也出现了请求日志。关掉测试后端,确认镜像失败不影响正常响应。
第 12 周:中间件链整合 + 收尾
Day 1:把所有功能重构为中间件
把鉴权、限流、UA 黑名单、内容过滤、Prometheus 指标、日志等逻辑都重构成独立的中间件函数,签名统一为 func(next http.Handler) http.Handler。比如 func TokenAuth(token string) func(http.Handler) http.Handler,检查 Token 不通过就返回 403,通过就调用 next.ServeHTTP(w, r)。
Day 2:串联中间件链
写一个 Chain 函数把多个中间件串起来。最终效果:请求依次经过 日志 → Prometheus → 限流 → Token 认证 → UA 黑名单 → 内容过滤 → 路由 → 负载均衡 → 熔断 → 转发。每一层都可以决定继续传递还是直接返回错误。
Day 3:学 flag 包 + 命令行参数
用 flag 包支持命令行参数:--config 指定配置文件路径,--port 指定监听端口。调用 flag.Parse() 解析。这样启动代理时可以 ./proxy --config /etc/proxy/config.json --port 9090。
Day 4:实现优雅关闭(Graceful Shutdown)
用 os/signal 监听 SIGINT 和 SIGTERM。收到信号后调用 server.Shutdown(ctx) 停止接受新连接,等待在途请求处理完再退出。同时关闭数据库连接、停止日志 goroutine。
Docker 化:写一个多阶段构建(Multi-stage Build)的 Dockerfile。第一阶段用 golang:1.22 编译,第二阶段用 scratch 或 gcr.io/distroless/static 作为底座,只复制编译好的二进制文件进去。Go 编译出来的是静态链接的单文件,打包出来的镜像可能只有 10-15MB。然后 docker build -t go-proxy . 构建,docker run -p 8080:8080 go-proxy 运行,确认代理在容器里正常工作。这个镜像可以直接推到你的 K3s 集群里跑。
Day 5:写单元测试 + 最终验收
学 testing 包。给限流器的 Allow() 写测试:连续调用 6 次,前 5 次返回 true 第 6 次返回 false。给熔断器写测试:连续 RecordResult(false) 超过阈值后 Allow() 返回 false。给轮询写测试:3 个后端调用 6 次,每个出现 2 次。用 go test ./... 运行。然后做一次完整的端到端验收。
进阶:gRPC 协议升级(第 13-14 周)
不需要重写代理。是在已有的中间件链和路由系统上加一个新的路由规则:当请求路径匹配 /grpc/... 时,走 gRPC 转发 handler;其他路径还是走原来的 HTTP 转发。限流、鉴权、Prometheus 指标这些中间件照常工作,gRPC 转换只是在链的末端替换了转发方式。
第 13 周:protobuf + gRPC 基础
Day 1:安装 protoc + 写第一个 .proto 文件
安装 Protocol Buffers 编译器(macOS 用 brew install protobuf),安装 Go 插件(go install google.golang.org/protobuf/cmd/protoc-gen-go@latest 和 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest)。写一个 greeter.proto,定义一个 Greeter 服务,有一个 SayHello 方法,请求参数是 name(string),返回值是 message(string)。
Day 2:生成 Go 代码 + 写 gRPC 服务端
运行 protoc --go_out=. --go-grpc_out=. greeter.proto 生成两个 .go 文件。写一个 gRPC 服务端:实现生成的接口,用 grpc.NewServer() 创建服务器,监听 50051 端口。学 google.golang.org/grpc 这个核心库。
Day 3:写 gRPC 客户端 + 验证通信
写一个 gRPC 客户端:用 grpc.Dial 连接服务端,调用 SayHello 方法。运行服务端和客户端,确认通信正常。理解完整链路:.proto 定义 → protoc 生成代码 → 服务端实现 → 客户端调用。
Day 4:学 JSON ↔ protobuf 转换
学 google.golang.org/protobuf/encoding/protojson 包。protojson.Unmarshal 把 JSON 转成 protobuf 消息,protojson.Marshal 把 protobuf 转回 JSON。写个小程序验证:JSON {"name": "world"} → protobuf HelloRequest → JSON,确认数据一致。
Day 5:学 gRPC 反射 + grpcurl
gRPC 反射让代理在运行时查询后端支持哪些方法和参数结构,不需要编译时知道 .proto 定义。在 gRPC 服务端开启反射(reflection.Register(server))。安装 grpcurl(brew install grpcurl),用它测试:grpcurl -plaintext localhost:50051 list 列出服务,grpcurl -plaintext -d '{"name": "world"}' localhost:50051 greeter.Greeter/SayHello 调用方法。
第 14 周:在代理中实现 HTTP→gRPC 转换
Day 1:学动态调用 API
学 google.golang.org/grpc 的 Invoke 方法和 google.golang.org/protobuf/types/dynamicpb 包。dynamicpb 允许在不 import 生成代码的情况下,根据运行时获取的服务描述符动态构造 protobuf 消息。这是通用 gRPC 代理的关键。
性能考量:动态反射转换(JSON ↔ Protobuf)虽然通用,但每次请求都要通过反射查询服务描述符,性能开销很大。可以加一个 LRU Cache(用 github.com/hashicorp/golang-lru 或自己用 sync.Map + 链表实现),缓存已经解析过的服务描述符和方法描述符。第一次请求某个方法时走反射,之后直接从缓存取,减少反射频率。
Day 2:在代理中加 gRPC 路由
加一个新路由规则 /grpc//。当请求匹配时:读取 JSON Body → 通过反射获取目标方法的描述符 → 用 protojson.Unmarshal 转成动态 protobuf 消息 → 用 grpc.Invoke 发送到后端 → 把 protobuf 响应用 protojson.Marshal 转回 JSON → 返回给客户端。
Day 3:处理 gRPC 错误码映射
gRPC 有自己的错误码(NotFound、Internal、Unavailable 等),需要映射到 HTTP 状态码:NotFound → 404,Internal → 500,Unavailable → 503。学 google.golang.org/grpc/status 包提取错误码。
Day 4:端到端测试
启动 gRPC 后端(开启反射),启动代理。用 curl -X POST -d '{"name": "world"}' localhost:8080/grpc/greeter.Greeter/SayHello 发送 HTTP+JSON 请求,确认代理翻译成 gRPC 发给后端,再翻译回 JSON 返回。客户端全程只用了 HTTP 和 JSON。
Day 5:最终收尾
验证 gRPC 路由和 HTTP 路由共存:/grpc/... 走 gRPC 转发,其他路径走 HTTP 转发。确认中间件链(限流、鉴权、Prometheus)对两种路由都生效。整理代码,更新 README。
14 周总览
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
阶段一:隐身斗篷(透明代理 / 协议转换)— 4 周
第 1 周 Go 基础语法 + 读 JSON 配置(多留 1 天给零基础缓冲)
第 2 周 HTTP 服务器 + 请求转发
第 3 周 伪装 Host + HTTPS 卸载
第 4 周 WebSocket 协议升级
阶段二:安检员(认证与过滤)— 2 周(压缩,语法简单)
第 5 周 Token 鉴权 + UA 黑名单 + 内容过滤
第 6 周 动态路由 + 轮询负载均衡
阶段三:交警(流量调度与限流)— 3 周(展开,并发是最陡的坡)
第 7 周 Go 并发模型(goroutine/channel/select)+ 限流器
第 8 周 熔断与降级(Mutex/context/状态机)
第 9 周 并发复习 + 耗时统计 + defer + 中间件模式入门
阶段四:监控探针(可观测性)— 3 周
第 10 周 Prometheus 指标 + 全量日志存数据库
第 11 周 流量镜像
第 12 周 中间件链整合 + 优雅关闭 + 单元测试
进阶:gRPC 协议升级 — 2 周
第 13 周 protobuf + gRPC 基础
第 14 周 在代理中实现 HTTP→gRPC 转换(不重写,加新路由)
推荐资源
A Tour of Go:语法速通,第 1 周前三天过完。
Go by Example:每个语法点一个可运行的例子,当字典查。
《Go 语言圣经》:系统学习,重点看 struct、interface、goroutine、channel 章节。
直接写代理项目:边做边学,遇到不会的语法再查,这是最快的路径。
一句话
14 周后你手里会有一个能伪装 Host、能卸载 HTTPS、能转发 WebSocket 和 gRPC、能鉴权、能过滤、能路由、能负载均衡、能限流、能熔断、能出 Prometheus 指标、能全量记录日志、能镜像流量的 Go 代理——装进你封装的 Linux 发行版里,就是一套完整的自动化运维体系。
---
## 在 Kubernetes 上用 Fluent Bit 收集 Nginx 日志到 Easysearch
- URL: https://blog.no-claw.com/posts/8fee9a49/
- 日期: 2026-03-10
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
本文基于 k3s + Easysearch 2.0.3 实测验证,从零开始搭建一套完整的日志收集方案。
什么是 Fluent Bit
Fluent Bit 是一个轻量级的日志收集和转发工具,用 C 语言写的,内存占用极低(通常只需要几十 MB)。它的工作很简单:从某个地方读日志(INPUT),可选地处理一下(FILTER),然后发到某个地方(OUTPUT)。
1
2
INPUT → FILTER → OUTPUT
读日志 处理 发送
常见用法:
从文件读日志(tail 插件,类似 tail -f)
从容器 stdout 读日志
发送到 Elasticsearch / Easysearch / Kafka / S3 等
和 Fluentd 的区别:Fluent Bit 更轻量(C 语言 vs Ruby),适合作为 Agent 部署在每个节点或 Pod 里。Fluentd 功能更丰富,适合做日志聚合层。在 Kubernetes 场景下,Fluent Bit 是更常见的选择。
什么是 Easysearch
INFINI Easysearch 是兼容 Elasticsearch API 的搜索引擎。Fluent Bit 的 es(Elasticsearch)输出插件可以直接对接,不需要改配置。简单理解:Easysearch 是 Elasticsearch 的国产替代品。
为什么用 Sidecar 模式
本文用 Sidecar 模式部署 Fluent Bit:把它和 Nginx 放在同一个 Pod 里,共享日志目录。
另一种常见方式是 DaemonSet 模式:在每个节点上跑一个 Fluent Bit,收集该节点上所有 Pod 的 stdout 日志。DaemonSet 适合收集所有 Pod 的日志,Sidecar 适合收集特定应用的日志文件。
1
2
3
4
5
Nginx Pod
├── nginx 容器 → 产生访问日志 → /var/log/nginx/access.log
└── fluent-bit 容器 → tail 日志文件 → 写入 Easysearch
↓
curl 查询 nginx-logs-* 索引
环境准备
k3s 单节点(Ubuntu 24.04,30G 内存)
Helm 3
cert-manager(Easysearch 依赖)
安装 k3s
1
2
3
4
5
6
7
8
curl -sfL https://get.k3s.io | sh -
# 配置 kubectl
mkdir -p ~/.kube
sudo chmod 644 /etc/rancher/k3s/k3s.yaml
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config
kubectl get nodes
安装 Helm
1
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
设置内核参数
Easysearch(基于 Lucene)需要较高的 mmap 限制:
1
2
sudo sysctl -w vm.max_map_count=262144
echo "vm.max_map_count=262144" | sudo tee -a /etc/sysctl.conf
第一步:部署 Easysearch
1.1 准备工具
更新helm仓库并且初始化cert-manager。
1
2
3
4
5
6
7
helm repo add infinilabs https://helm.infinilabs.com
helm repo update
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.3/cert-manager.yaml
# 等所有 Pod Ready(约 30-60 秒)
kubectl get pods -n cert-manager -w
1.2 创建命名空间和证书
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
kubectl create namespace es
# 创建自签名 CA
cat << EOF | kubectl -n es apply -f -
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: easysearch-ca-issuer
spec:
selfSigned: {}
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: easysearch-ca-certificate
spec:
commonName: easysearch-ca-certificate
duration: 87600h0m0s
isCA: true
issuerRef:
kind: Issuer
name: easysearch-ca-issuer
privateKey:
algorithm: ECDSA
size: 256
renewBefore: 2160h0m0s
secretName: easysearch-ca-secret
EOF
1.3 创建密码 Secret
⚠️ 密码必须包含至少 2 类字符(大写/小写/数字/特殊字符),否则初始化会失败,Pod 直接 Exit Code 1 崩溃。
1
2
kubectl create secret generic easysearch-secrets \
-n es --from-literal=ezs_password='Admin123'
1.4 Helm 安装 Easysearch
使用厂家提供了helm Chart安装Easysearch,并且通过变量设置image的版本,截止目前,最新版本是2.0.3-2534。
1
2
3
4
5
6
helm repo add infinilabs https://helm.infinilabs.com --force-update
helm repo update
helm install easysearch infinilabs/easysearch -n es \
--set image.tag=2.0.3-2534
然后就是等 Pod Running:
1
2
3
kubectl get pods -n es -w
# NAME READY STATUS RESTARTS AGE
# easysearch-0 1/1 Running 0 60s
1.5 验证 Easysearch
1
2
3
kubectl port-forward -n es pod/easysearch-0 9200:9200 &
sleep 3
curl -s http://localhost:9200 -u admin:Admin123
返回 JSON 集群信息即成功:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"name" : "easysearch-0",
"cluster_name" : "infinilabs",
"cluster_uuid" : "_eqbd5PbQ5WNpNVJFccNDA",
"version" : {
"distribution" : "easysearch",
"number" : "2.0.3",
"distributor" : "INFINI Labs",
"build_hash" : "e6180819aedb3d4759cdbcd2c1b856a9635d4aff",
"build_date" : "2026-01-16T09:44:14.477332385Z",
"build_snapshot" : false,
"lucene_version" : "9.12.2",
"minimum_wire_lucene_version" : "8.7.0",
"minimum_lucene_index_compatibility_version" : "8.7.0"
},
"tagline" : "You Know, For Easy Search!"
}
如果你的机器内存比较小(< 4G)或者使用其他 storageClassName 需要修改配置:
1
2
3
4
5
helm install easysearch infinilabs/easysearch -n es \
--set storageClassName=gp2 \
--set resources.requests.memory=512Mi \
--set resources.limits.memory=1536Mi \
--set "javaOpts=-Xms256m -Xmx256m"
第二步:部署 Nginx + Fluent Bit Sidecar
这个 YAML 包含三个 Kubernetes 资源(用 --- 分隔):
ConfigMap:存放 Fluent Bit 的配置文件 fluent-bit.conf
Deployment:定义一个 Pod,里面跑两个容器(nginx + fluent-bit sidecar)
Service:让集群内其他 Pod 可以通过 http://nginx:80 访问 Nginx
创建 nginx-fluentbit.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
cat << 'EOF' > nginx-fluentbit.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: fluent-bit-config
namespace: default
data:
fluent-bit.conf: |
[SERVICE]
Flush 3
Log_Level info
Daemon off
[INPUT]
Name tail
Tag nginx.access
Path /var/log/nginx/access.log
DB /var/log/flb_nginx.db
Mem_Buf_Limit 5MB
Refresh_Interval 5
[OUTPUT]
Name es
Match *
Host easysearch.es.svc.cluster.local
Port 9200
HTTP_User admin
HTTP_Passwd Admin123
tls Off
Logstash_Format On
Logstash_Prefix nginx-logs
Retry_Limit False
Suppress_Type_Name On
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: nginx-logs
mountPath: /var/log/nginx
- name: fluent-bit
image: fluent/fluent-bit:2.2
volumeMounts:
- name: nginx-logs
mountPath: /var/log/nginx
readOnly: true
- name: config
mountPath: /fluent-bit/etc/
volumes:
- name: nginx-logs
emptyDir: {}
- name: config
configMap:
name: fluent-bit-config
---
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: default
spec:
selector:
app: nginx
ports:
- port: 80
targetPort: 80
type: ClusterIP
EOF
部署和删除
1
2
3
4
5
# 部署
kubectl apply -f nginx-fluentbit.yaml
# 删除
kubectl delete -f nginx-fluentbit.yaml
确认 Pod 里两个容器都 Running(READY 2/2):
1
2
3
kubectl get pods
# NAME READY STATUS RESTARTS AGE
# nginx-699c4dd7ff-ntzfg 2/2 Running 0 30s
YAML 逐字段解释
ConfigMap 部分(Fluent Bit 配置):
字段
值
说明
[SERVICE] Flush
3
每 3 秒把缓冲区的日志刷到 OUTPUT
Log_Level
info
Fluent Bit 自身的日志级别
Daemon
off
前台运行,容器里必须前台否则容器会退出
[INPUT] Name
tail
类似 tail -f,实时跟踪文件新增内容
Tag
nginx.access
给输入打标签,OUTPUT 用 Match 匹配
Path
/var/log/nginx/access.log
要读的日志文件路径
DB
/var/log/flb_nginx.db
SQLite 文件,记录读到哪一行了,重启不重复读
Mem_Buf_Limit
5MB
内存缓冲区上限,防止日志太多撑爆内存
Refresh_Interval
5
每 5 秒检查文件是否有新内容
[OUTPUT] Name
es
Elasticsearch 兼容输出插件,Easysearch 直接可用
Match
*
匹配所有 Tag 的日志
Host
easysearch.es.svc.cluster.local
Easysearch 的 Service DNS(集群内访问)
HTTP_User/Passwd
admin/Admin123
Easysearch 认证信息
tls
Off
不用 HTTPS(本版本默认 HTTP)
Logstash_Format
On
按日期创建索引,格式:nginx-logs-2026.03.11
Logstash_Prefix
nginx-logs
索引名前缀
Retry_Limit
False
发送失败无限重试
Suppress_Type_Name
On
兼容 ES 7.x+,不发送 _type
⚠️ Fluent Bit 的 ini 格式不支持行内注释!Flush 3 # 注释 会把 3 # 注释 整个当成值,导致解析失败。
Deployment 部分(两个容器 + 共享卷):
字段
说明
containers[0]: nginx
Nginx 容器,处理 HTTP 请求,日志写到 /var/log/nginx/access.log
containers[1]: fluent-bit
Sidecar 容器,读取 nginx 的日志文件发送到 Easysearch
volumeMounts: nginx-logs
两个容器都挂载这个卷,共享 /var/log/nginx 目录
readOnly: true
Fluent Bit 只读挂载,最小权限原则
volumeMounts: config
把 ConfigMap 挂载为 Fluent Bit 的配置文件目录
volumes: nginx-logs (emptyDir)
临时卷,Pod 内容器共享,Pod 删除后数据丢失
volumes: config (configMap)
引用上面的 ConfigMap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
同一个 Pod
┌─────────────────────────────────────────────────┐
│ │
│ nginx 容器 fluent-bit 容器 │
│ ┌──────────┐ ┌──────────────┐ │
│ │ 处理请求 │ 写入 │ tail 插件 │ │
│ │ │ ────────→ │ 读取日志文件 │ │
│ │ access │ emptyDir │ │ │
│ │ .log │ 共享卷 │ es 插件 │ │
│ └──────────┘ │ 发送到 │ │
│ │ Easysearch │ │
│ └──────┬───────┘ │
│ │ │
└───────────────────────────────────┼─────────────┘
│
↓
┌──────────────────┐
│ Easysearch │
│ nginx-logs-* │
│ 索引 │
└──────────────────┘
关键概念
为什么用 emptyDir?
emptyDir 是一个临时卷,在 Pod 创建时自动创建,Pod 删除时自动清理。它的作用是让同一个 Pod 里的多个容器共享文件。nginx 写日志到这个目录,fluent-bit 从这个目录读日志。
为什么 fluent-bit 挂载时加 readOnly: true?
Fluent Bit 只需要读日志文件,不需要写。加 readOnly 是最小权限原则,防止 Fluent Bit 意外修改日志文件。
Logstash_Format On 是什么意思?
开启后 Fluent Bit 会按日期自动创建索引,格式为 {Logstash_Prefix}-{日期},比如 nginx-logs-2026.03.11。这样每天的日志在不同索引里,方便按时间范围查询和清理旧数据。
DB /var/log/flb_nginx.db 是什么?
Fluent Bit 用这个 SQLite 数据库文件记录”读到文件的哪一行了”。如果 Fluent Bit 重启,它会从上次的位置继续读,不会重复发送已经处理过的日志。
第三步:产生日志并查询
3.1 产生访问日志
1
2
kubectl run curl-test --rm -it --restart=Never --image=curlimages/curl -- \
sh -c 'for i in $(seq 1 20); do curl -s http://nginx > /dev/null; sleep 0.5; done'
3.2 查询 Easysearch
1
2
3
4
5
kubectl port-forward -n es pod/easysearch-0 9200:9200 &
sleep 2
curl -s 'http://localhost:9200/nginx-logs-*/_search?size=3' \
-u admin:Admin123 | python3 -m json.tool
返回结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"hits": {
"total": { "value": 31 },
"hits": [
{
"_index": "nginx-logs-2026.03.11",
"_source": {
"@timestamp": "2026-03-11T07:16:55.881Z",
"log": "10.42.0.22 - - [11/Mar/2026:07:16:55 +0000] \"GET / HTTP/1.1\" 200 896 \"-\" \"curl/8.18.0\" \"-\""
}
}
]
}
}
3.3 从本地电脑访问(SSH 隧道)
Easysearch 自带了一个UI,但是跑在远程POD里,部署的Service也是Headless,所以没办法直接访问,我们的办法是先用kubectl port-forward把pod端口映射到服务器宿主机,然后再用 SSH 隧道把 9200 端口映射到本地:
1
2
3
4
5
6
7
8
# 远程服务器上先跑 port-forward
kubectl port-forward -n es pod/easysearch-0 9200:9200 &
# 本地电脑 SSH 隧道
ssh -L 9200:localhost:9200 ubuntu@ -i key.pem
# 然后本地直接访问
curl -s http://localhost:9200/nginx-logs-*/_search?size=3 -u admin:Admin123
这样转发之后,我们也可以使用本机浏览器直接访问Easysearch自带的UI了。
踩坑记录
1. 密码复杂度(最常见的坑)
Easysearch 最新版本要求密码至少包含 2 类字符,不满足时 Pod 直接崩溃(Exit Code 1),日志来不及写,非常难排查。
1
2
3
4
5
# ❌ 纯小写
ezs_password='easysearchpaswd'
# ✅ 大写 + 小写 + 数字
ezs_password='Admin123'
2. vm.max_map_count
不设置的话 Easysearch 启动失败:
1
sudo sysctl -w vm.max_map_count=262144
3. 小内存环境 OOMKilled
默认 JVM 堆 1G + 堆外内存,总共需要 2G+。小集群用:
1
2
3
--set "javaOpts=-Xms256m -Xmx256m"
--set resources.requests.memory=512Mi
--set resources.limits.memory=1536Mi
4. StorageClass 不匹配(AWS 环境)
我的自建集群没有默认 StorageClass 是 gp2,需要指定:
1
--set storageClassName=gp2
如果环境自带 local-path-provisioner,不需要指定。
5. Fluent Bit 配置不支持行内注释
Fluent Bit 的 ini 格式不支持行内注释,# 后面的内容会被当成值的一部分:
1
2
3
4
5
6
# ❌ 错误:行内注释会被当成值
Logstash_Format On # 按日期创建索引
# ✅ 正确:注释单独一行
# 按日期创建索引
Logstash_Format On
排查命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Pod 状态
kubectl get pods -n es -w
kubectl describe pod easysearch-0 -n es
# 容器日志
kubectl logs easysearch-0 -n es
kubectl logs easysearch-0 -n es --previous
kubectl logs easysearch-0 -n es -c init-config
# Fluent Bit sidecar 日志
kubectl logs -c fluent-bit
# 退出原因
kubectl describe pod easysearch-0 -n es | grep -A5 "Last State"
# Exit Code 1 = 应用错误(密码/配置)
# Exit Code 137 = OOMKilled(内存不足)
# 资源使用
kubectl top nodes
kubectl top pods -n es
free -h
# 确认 TLS 模式
kubectl logs easysearch-0 -n es | grep "TLS HTTP Provider"
# null = HTTP,JDK = HTTPS
# 确认密码
kubectl get secret easysearch-secrets -n es \
-o jsonpath='{.data.ezs_password}' | base64 -d
# 确认镜像版本
kubectl get pod easysearch-0 -n es -o jsonpath='{.spec.containers[0].image}'
清理
1
2
3
4
5
6
7
8
# 删除 Nginx + Fluent Bit
kubectl delete -f nginx-fluentbit.yaml
# 删除 Easysearch
helm uninstall easysearch -n es
kubectl delete pvc --all -n es
kubectl delete secret easysearch-secrets -n es
kubectl delete namespace es
---
## 在AWS EC2 上从零搭建 Kubernetes 集群(kubeadm)
- URL: https://blog.no-claw.com/posts/7e9d67ce/
- 日期: 2026-03-05
- 分类: AWS
- 标签: AWS
今天讲解在AWS EC2 上使用kubeadm搭建Kubernetes 集群。
kubeadm 是 Kubernetes 官方提供的集群引导工具,用来快速创建符合最佳实践的 K8s 集群。除了初始化集群,它还能做节点的升级、降级等生命周期管理。用 kubeadm 建集群是学习 K8s 的推荐方式,也适合搭建小规模集群或作为更复杂企业级方案的基础组件。
本文基于 Ubuntu,使用三台 EC2 实例:一台作为控制面(Master),两台作为工作节点(Worker)。
我们会在 Master 节点上从头安装 kubeadm 及其依赖,然后初始化集群,最后把 Worker 节点加入进来。
这几台 EC2 使用同一个安全组,入站规则只放行VPC网段和终端访问地址。
第一步:安装 kubeadm 及其依赖
以下操作在 Master 节点上执行(Worker 节点如果还没装也要跑一遍)。
1. 关闭 swap
kubelet 需要精确管理 Pod 的内存,swap 会让内存数据不准确,导致调度器做出错误判断。kubelet 默认检测到 swap 开着就拒绝启动。
1
2
3
4
# 关闭 swap
sudo swapoff -a
# 注释掉 fstab 里的 swap 行,重启后也不会再开
sudo sed -i '/ swap / s/^/#/' /etc/fstab
验证:free -h,Swap 行全是 0 就对了。
1
2
3
4
5
ubuntu@ip-172-31-27-240:~$ free -h
total used free shared buff/cache available
Mem: 1.9Gi 361Mi 1.4Gi 2.7Mi 303Mi 1.5Gi
Swap: 0B 0B 0B
ubuntu@ip-172-31-27-240:~$
2. 更新包管理器,安装基础依赖
更新 apt 包索引,安装通过 HTTPS 访问软件仓库所需的包:
1
2
sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl software-properties-common gnupg lsb-release
3. 加载内核模块,允许 IPv4 转发
加载 overlay 和 br_netfilter 两个内核模块。overlay 是 containerd 存储驱动需要的,br_netfilter 让 bridge 上的流量能经过 iptables 规则,K8s 的 Service 和 NetworkPolicy 都依赖这个。
1
2
3
4
5
6
7
sudo modprobe overlay
sudo modprobe br_netfilter
cat < /dev/null
sudo apt-get update
sudo apt-get install -y containerd.io
6. 配置 systemd cgroup 驱动
生成 containerd 默认配置,然后把 cgroup 驱动改成 systemd。这一步是为了避免系统里同时存在两个 cgroup 管理器(systemd 和 cgroupfs)导致的不稳定问题。kubelet 默认用 systemd,containerd 也要对齐。
1
2
3
4
5
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sudo systemctl restart containerd
sudo systemctl enable containerd
7. 安装 kubeadm、kubelet、kubectl
从 Kubernetes 官方包仓库安装。三个组件的作用:
kubeadm:集群引导工具,负责 init 和 join
kubelet:每个节点上的代理,负责管理 Pod 和容器
kubectl:命令行工具,用来操作集群
下面以 v1.30 为例。如果要装其他版本(比如 v1.35),把两处 v1.30 都改成 v1.35 就行,GPG key 和源地址的版本号要一致。可用版本列表见 https://pkgs.k8s.io/core:/stable:/
1
2
3
4
5
6
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
8. 锁定版本,防止自动升级
K8s 组件版本需要严格对齐,自动升级可能导致版本不一致出问题:
1
sudo apt-mark hold kubelet kubeadm kubectl
第二步:初始化控制面节点
以下操作仅在 Master 节点执行。
初始化过程会完成以下事情:
创建证书颁发机构(CA),用于集群内的安全通信和身份认证
启动节点组件:kubelet
启动控制面组件:API Server、Controller Manager、Scheduler、etcd
安装通用插件:kube-proxy、DNS
kubeadm 初始化使用合理的默认值,遵循最佳实践。当然也有很多配置选项可以自定义,比如使用自己的 CA 证书或外部 etcd 存储。
kubeadm 不会帮你安装网络插件,这个需要自己装。我们用 Calico 作为 Pod 网络插件。Calico 支持 Kubernetes NetworkPolicy,也是 AWS、Azure、GCP 的托管 K8s 服务内部使用的网络方案,生产可用。为了让 Calico 的网络策略正常工作,初始化时必须通过 --pod-network-cidr 指定 Pod 网络的 IP 范围。
1. 初始化控制面
1
sudo kubeadm init --pod-network-cidr=192.168.0.0/16
192.168.0.0/16 是 Calico 的默认网段。注意这个网段不能跟你的 VPC 网络 CIDR 重叠,如果重叠了需要额外配置 Calico 来避免冲突。
输出会显示 kubeadm 初始化控制面的每一步。最后会给出两个重要信息:配置 kubectl 的命令和 worker 节点加入集群的 join 命令。
2. 保存 join 命令
把输出最后的 kubeadm join ... 命令复制下来存好,后面 worker 节点加入集群要用。token 默认 24 小时过期,过期了可以用 kubeadm token create --print-join-command 重新生成。
3. 配置 kubectl
用 kubeadm 生成的 admin kubeconfig 初始化 kubectl 配置:
1
2
3
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
4. 确认控制面组件状态
1
kubectl get componentstatuses
输出应该显示 scheduler、controller-manager、etcd 都是 Healthy。kubectl 能正常返回结果也说明 API Server 在正常工作。
5. 查看节点状态
1
kubectl get nodes
这时候控制面节点会显示 NotReady。这是正常的——因为还没装网络插件。
1
2
3
NAME STATUS ROLES AGE VERSION
ip-172-31-16-97 NotReady 33s v1.30.14
ip-172-31-27-240 NotReady control-plane 4m11s v1.30.14
可以用 kubectl describe nodes 看详细信息,Conditions 里会显示 Ready: False,原因是 “network plugin is not ready”,CNI(Container Network Interface)还没初始化。
6. 安装 Calico 网络插件(Operator 方式)
Calico 有两种安装方式:Operator 和 Manifest。官方现在推荐 Operator 方式,通过 Tigera Operator 来管理 Calico 的生命周期,后续升级和配置变更都更方便。
先装 Tigera Operator:
1
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.29.0/manifests/tigera-operator.yaml
再装 Calico 自定义资源:
1
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.29.0/manifests/custom-resources.yaml
custom-resources.yaml 里默认的 Pod CIDR 是 192.168.0.0/16,跟 kubeadm init 时指定的一致。如果你用了不同的 CIDR,需要先下载这个文件改 cidr 字段再 apply。
这会创建一系列资源来支持 Pod 网络,包括:
Tigera Operator Deployment,负责管理 Calico 组件的部署和升级
一个 DaemonSet,在集群每个节点上运行一个 calico-node Pod
多个 CRD(自定义资源定义),扩展 K8s API 来支持网络策略等功能
Calico 组件会部署在 calico-system namespace 下(Operator 方式),而不是 kube-system(Manifest 方式)。
7. 等待节点就绪
1
watch kubectl get nodes
1
2
3
4
NAME STATUS ROLES AGE VERSION
ip-172-31-16-97 Ready 95s v1.30.14
ip-172-31-27-240 Ready control-plane 5m13s v1.30.14
网络插件初始化完成后,控制面节点会变成 Ready。可能需要等一分钟左右。按 Ctrl+C 停止 watch。
第三步:Worker 节点加入集群
以下操作在 Worker 节点执行。
使用 kubeadm 添加工作节点比初始化控制面还要简单,一条 join 命令就搞定。
1. 连接到 Worker 节点
打开一个新终端,SSH 连接到 Worker 节点(用户名 ubuntu)。
2. 执行 join 命令
用 sudo 加上之前从 kubeadm init 输出中保存的 join 命令:
1
2
3
sudo kubeadm join 10.0.0.100:6443 \
--token \
--discovery-token-ca-cert-hash sha256:
如果 token 过期了(24 小时有效),在 Master 上重新生成:
1
kubeadm token create --print-join-command
3. 确认 Worker 节点加入集群
回到控制面节点的终端,查看节点状态:
1
kubectl get nodes
Worker 节点会显示出来,ROLES 列为 (这是正常的,worker 节点默认没有角色标签)。节点可能需要一分钟左右才能变成 Ready。
4. 确认所有 Pod 正常运行
1
kubectl get pods --all-namespaces
所有 Pod 都应该处于 Running 状态。注意每个节点上都会有 Calico 的 Pod 在运行,负责该节点的网络功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ubuntu@ip-172-31-27-240:~$ kubectl get pods --all-namespaces
NAMESPACE NAME READY STATUS RESTARTS AGE
calico-apiserver calico-apiserver-7458595b77-jclzd 1/1 Running 0 61s
calico-apiserver calico-apiserver-7458595b77-w2ww8 1/1 Running 0 61s
calico-system calico-kube-controllers-8b7bdfdc7-tgx2p 1/1 Running 0 60s
calico-system calico-node-d4m6v 1/1 Running 0 61s
calico-system calico-node-lr6bq 1/1 Running 0 61s
calico-system calico-typha-5494555c85-5vftk 1/1 Running 0 61s
calico-system csi-node-driver-spgw8 2/2 Running 0 60s
calico-system csi-node-driver-xjsfk 2/2 Running 0 60s
kube-system coredns-55cb58b774-fjhvm 1/1 Running 0 5m28s
kube-system coredns-55cb58b774-z4c6x 1/1 Running 0 5m28s
kube-system etcd-ip-172-31-27-240 1/1 Running 0 5m41s
kube-system kube-apiserver-ip-172-31-27-240 1/1 Running 0 5m42s
kube-system kube-controller-manager-ip-172-31-27-240 1/1 Running 0 5m41s
kube-system kube-proxy-424ck 1/1 Running 0 5m28s
kube-system kube-proxy-7tpcs 1/1 Running 0 2m7s
kube-system kube-scheduler-ip-172-31-27-240 1/1 Running 0 5m41s
tigera-operator tigera-operator-5645cfc98-fjxtp 1/1 Running 0 67s
双节点集群到这里就搭建完成了。
第四步:部署应用验证集群
集群搭好了,跑个 nginx 验证一下。
1. 创建 nginx Pod
1
kubectl run nginx --image=nginx --port=80
2. 确认 Pod 运行正常
1
kubectl get pods -o wide
1
2
3
ubuntu@ip-172-31-27-240:~$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx 1/1 Running 0 9s 192.168.98.135 ip-172-31-16-97
看 STATUS 是 Running,IP 已分配,记下 Pod 跑在哪个节点上。
3. 通过 NodePort 暴露服务
1
2
kubectl expose pod nginx --type=NodePort --port=80
kubectl get svc nginx
输出里 PORT(S) 列会显示类似 80:31973/TCP,31973 就是 NodePort。
4. 访问 nginx
1
2
# 用 Pod 所在节点的 IP + NodePort
curl :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ubuntu@ip-172-31-27-240:~$ curl 172.31.16.97:32686
Welcome to nginx!
Welcome to nginx!
If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.
For online documentation and support please refer to
nginx.org.
Commercial support is available at
nginx.com.
Thank you for using nginx.
能看到 nginx 欢迎页就说明集群网络正常。
注意:NodePort 通过节点 IP 访问,不是 ClusterIP。ClusterIP 是集群内部的虚拟 IP,只在集群内部可达。
AWS 上的 Calico 网络踩坑
在 AWS EC2 上用 Calico 默认的 BGP 路由模式,你可能会发现从一个节点直接 curl 另一个节点上的 Pod IP 不通。比如从 master(10.0.0.100)curl worker 上的 Pod(192.168.180.194)会超时。
路由表是对的(ip route | grep 192.168 能看到到 worker Pod 网段的路由),但包就是过不去。
原因是 AWS VPC 的源/目标检查(Source/Destination Check)。EC2 默认会检查每个网络包的源 IP 和目标 IP 是否属于这台实例,Pod IP(192.168.x.x)不属于 EC2 的 VPC 地址,VPC 直接把包丢了。
关闭源/目标检查(简单)
AWS Console → EC2 → 选中实例 → Actions → Networking → Change source/destination check → Stop
所有节点都要改。改完后跨节点的 Pod IP 直接访问就通了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ubuntu@ip-172-31-27-240:~$ curl 192.168.98.135
Welcome to nginx!
Welcome to nginx!
If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.
For online documentation and support please refer to
nginx.org.
Commercial support is available at
nginx.com.
Thank you for using nginx.
第五步:用 Deployment 管理副本
前面用 kubectl run 创建的是裸 Pod,不支持扩缩容。生产环境应该用 Deployment,支持副本管理和滚动更新。
1. 删掉裸 Pod,改用 Deployment
1
2
3
4
5
6
kubectl delete pod nginx
kubectl delete svc nginx
# 创建 Deployment,10 个副本
kubectl create deployment nginx --image=nginx --replicas=10
kubectl expose deployment nginx --type=NodePort --port=80
2. 查看副本状态
1
2
kubectl get deployment nginx
kubectl get pods -o wide
10 个 Pod 会分布在不同节点上。
3. 验证所有 Pod 都能访问
一行命令逐个 curl 所有 Pod IP:
1
kubectl get pods -l app=nginx -o jsonpath='{range .items[*]}{.status.podIP}{"\n"}{end}' | while read ip; do echo "--- $ip ---"; curl --connect-timeout 3 -s $ip | head -1; done
每个 IP 都输出 就说明都通了。
4. 调整副本数
1
2
3
4
5
# 缩到 3 个
kubectl scale deployment nginx --replicas=3
# 扩到 20 个
kubectl scale deployment nginx --replicas=20
附:安装 k9s(终端集群管理工具)
k9s 是一个终端 UI 工具,比 kubectl 看集群状态直观很多,支持实时查看 Pod、日志、资源占用等。
Mac:
1
brew install derailed/k9s/k9s
Ubuntu/Linux:
1
curl -sS https://webi.sh/k9s | sh
或者直接下载二进制:
1
2
3
curl -LO https://github.com/derailed/k9s/releases/latest/download/k9s_Linux_amd64.tar.gz
tar xzf k9s_Linux_amd64.tar.gz
sudo mv k9s /usr/local/bin/
装完敲 k9s 进入,按 : 输入资源类型(比如 pods、deploy、svc)切换视图从而看到集群的各种信息,q 退出。
---
## 修复 GitHub Pages 推送后 CNAME 自动重置旧域名的问题
- URL: https://blog.no-claw.com/posts/6ec894ce/
- 日期: 2026-03-04
- 分类: Blog
- 标签: Blog
Hexo 使用Github action 部署后 GitHub Pages 域名自动变成已经取消的实效的域名,而不是预期的 *.github.io
排查之后是source/CNAME 文件中配置了旧域名 airag.click
删除 CNAME 文件后重新部署就可以解决
1
rm source/CNAME
CNAME 文件会被 Hexo 复制到生成目录,告诉 GitHub Pages 使用自定义域名
删除后将使用默认的 .github.io 域名
---
## k3s + Helm 部署 Easysearch
- URL: https://blog.no-claw.com/posts/1dba38bc/
- 日期: 2026-03-02
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
最近学了K8S,为了测试方便测试搭了一个K3S集群,然后使用helm运行一下Easysearch。
参考文档:https://docs.infinilabs.com/easysearch/main/docs/deployment/install-guide/helm/
首先添加helm仓库并更新。
1
2
helm repo add infinilabs https://helm.infinilabs.com
helm repo update
然后新建命名空间,我这里叫做es(下同),也可以使用其他名字。
1
kubectl create namespace es
Easysearch 依赖 cert-manager 来处理证书。使用这个命令来安装。
1
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.3/cert-manager.yaml
否则就会收到如下报错。
1
2
3
4
5
6
7
resource mapping not found for name: "easysearch-ca-issuer" namespace: "" from "STDIN": no matches for kind "Issuer" in version "cert-manager.io/v1"
ensure CRDs are installed first
resource mapping not found for name: "easysearch-ca-certificate" namespace: "" from "STDIN": no matches for kind "Certificate" in version "cert-manager.io/v1"
ensure CRDs are installed first
直接执行以下命令,我设置的命名空间固定是 es:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
cat << EOF | kubectl apply -n es -f -
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: easysearch-ca-issuer
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: easysearch-ca-certificate
spec:
commonName: easysearch-ca-certificate
duration: 87600h0m0s
isCA: true
issuerRef:
kind: Issuer
name: easysearch-ca-issuer
privateKey:
algorithm: ECDSA
size: 256
renewBefore: 2160h0m0s
secretName: easysearch-ca-secret
EOF
Easysearch Chart 默认开启了安全功能,需要在 es 命名空间下找到一个名为 easysearch-secrets 的 Secret,用来存放集群的初始化密码或通信密钥。
之前创建的是 easysearch-ca-secret(CA 证书),但系统还在找这个基础的 easysearch-secrets。
使用这个创建集群需要的secret:
1
2
3
kubectl create secret generic easysearch-secrets -n es \
--from-literal=ezs_username=admin \
--from-literal=ezs_password=easysearchpaswd
另外在启动集群之前,别忘记修改max_map_count:
1
2
3
4
5
# 临时生效
sudo sysctl -w vm.max_map_count=262144
# 永久生效(防止重启失效)
echo "vm.max_map_count=262144" | sudo tee -a /etc/sysctl.conf
都准备了好了之后我们来使用helm能够很方便的安装Easysearch:
1
helm install easysearch infinilabs/easysearch -n es
如果你的某个步骤有问题,修改配置之后需要重启,那么可以直接删除这个pod,然后K3S会自动按照当前配置拉起来一个最新的。
1
kubectl delete pod easysearch-0 -n es
在这个过程过,我发现helm里的Easysearch版本比较旧
不需要 uninstall,直接运行 upgrade。这样子就会触发 Kubernetes 的 **RollingUpdate (滚动更新)**:它会先停掉旧的 Pod,挂载原来的数据卷,然后启动 2.0.2 的新容器。
1
2
3
4
# 使用 upgrade 命令,强行覆盖镜像 Tag
helm upgrade easysearch infinilabs/easysearch -n es \
--reuse-values \
--set image.tag=2.0.2
然后可以使用helm继续安装console。
1
helm install console infinilabs/console -n es
我的集群开了http端口,可以进入pod进去call api。但是https没有反应。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
kubectl exec -it easysearch-0 -n es -- curl -u admin:easysearchpaswd http://127.0.0.1:9200
Defaulted container "easysearch" out of: easysearch, init-config (init)
{
"name" : "easysearch-0",
"cluster_name" : "infinilabs",
"cluster_uuid" : "oqn-k99eS32e0IRMlxrcHg",
"version" : {
"distribution" : "easysearch",
"number" : "1.13.0",
"distributor" : "INFINI Labs",
"build_hash" : "5b73b39bc689f1366b09987fa07eee07ee89c2f6",
"build_date" : "2025-06-11T07:39:43.374688Z",
"build_snapshot" : false,
"lucene_version" : "8.11.4",
"minimum_wire_lucene_version" : "7.7.0",
"minimum_lucene_index_compatibility_version" : "7.7.0"
},
"tagline" : "You Know, For Easy Search!"
}
kubectl exec -it easysearch-0 -n es -- curl -k -u admin:easysearch https://127.0.0.1:9200
Defaulted container "easysearch" out of: easysearch, init-config (init)
curl: (35) error:0A0000C6:SSL routines::packet length too long
command terminated with exit code 35
如果想清理数据或者重装,可以使用这些命令。
1
2
3
4
5
6
7
8
9
# 卸载应用
helm uninstall easysearch console -n es
# 清理残留的数据卷 (数据会被删除,请谨慎操作)
kubectl delete pvc -n es \
easysearch-data-easysearch-0 \
easysearch-config-easysearch-0 \
console-data-console-0 \
console-config-console-0 \
那么在pod中我们怎么访问Easysearch呢?
我用busybox 当做例子,写了三种访问的方式
使用POD的IP地址
使用service的名字访问
使用pod.service访问(其实就是Headless Service)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
kubectl run busybox-debug --rm -it --image=busybox -n es -- /bin/sh
All commands and output from this session will be recorded in container logs, including credentials and sensitive information passed through the command prompt.
If you don't see a command prompt, try pressing enter.
/ # curl
/bin/sh: curl: not found
# 1:使用POD的IP地址
/ # wget -qO- http://admin:easysearchpaswd@10.42.0.78:9200
{
"name" : "easysearch-0",
"cluster_name" : "infinilabs",
"cluster_uuid" : "c7pjDjA4SJSQixsbrdy1GQ",
"version" : {
"distribution" : "easysearch",
"number" : "1.13.0",
"distributor" : "INFINI Labs",
"build_hash" : "5b73b39bc689f1366b09987fa07eee07ee89c2f6",
"build_date" : "2025-06-11T07:39:43.374688Z",
"build_snapshot" : false,
"lucene_version" : "8.11.4",
"minimum_wire_lucene_version" : "7.7.0",
"minimum_lucene_index_compatibility_version" : "7.7.0"
},
"tagline" : "You Know, For Easy Search!"
}
#2. 使用service的名字访问
/ # wget -qO- http://admin:easysearchpaswd@easysearch:9200
{
"name" : "easysearch-0",
"cluster_name" : "infinilabs",
"cluster_uuid" : "c7pjDjA4SJSQixsbrdy1GQ",
"version" : {
"distribution" : "easysearch",
"number" : "1.13.0",
"distributor" : "INFINI Labs",
"build_hash" : "5b73b39bc689f1366b09987fa07eee07ee89c2f6",
"build_date" : "2025-06-11T07:39:43.374688Z",
"build_snapshot" : false,
"lucene_version" : "8.11.4",
"minimum_wire_lucene_version" : "7.7.0",
"minimum_lucene_index_compatibility_version" : "7.7.0"
},
"tagline" : "You Know, For Easy Search!"
}
#3. 使用pod.service访问
/ # wget -qO- http://admin:easysearchpaswd@easysearch-0.easysearch:9200
{
"name" : "easysearch-0",
"cluster_name" : "infinilabs",
"cluster_uuid" : "c7pjDjA4SJSQixsbrdy1GQ",
"version" : {
"distribution" : "easysearch",
"number" : "1.13.0",
"distributor" : "INFINI Labs",
"build_hash" : "5b73b39bc689f1366b09987fa07eee07ee89c2f6",
"build_date" : "2025-06-11T07:39:43.374688Z",
"build_snapshot" : false,
"lucene_version" : "8.11.4",
"minimum_wire_lucene_version" : "7.7.0",
"minimum_lucene_index_compatibility_version" : "7.7.0"
},
"tagline" : "You Know, For Easy Search!"
}
---
## k8s yml小抄
- URL: https://blog.no-claw.com/posts/e2ac0d70/
- 日期: 2026-03-01
- 分类: 软件
- 标签: Kubernetes
源码来自 cloudacademy/intro-to-k8s,用作学习笔记
1. Pod 基础
1.1 最简 Pod(1.1-basic_pod.yaml)
1
2
3
4
5
6
7
8
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: mycontainer
image: nginx:latest
这是最小化的 Pod 定义。只需要四个顶级字段:
apiVersion: v1 — 核心 API 版本
kind: Pod — 资源类型
metadata.name — Pod 名称,命名空间内唯一
spec.containers — 至少一个容器,必须指定 name 和 image
注意:使用 nginx:latest 时,Kubernetes 默认 imagePullPolicy: Always,每次启动都会拉取镜像。
1.2 声明端口(1.2-port_pod.yaml)
1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: mycontainer
image: nginx:latest
ports:
- containerPort: 80
相比 1.1 新增了 ports.containerPort: 80。如果不声明端口,kubectl describe 中端口显示为 none,外部无法知道容器监听哪个端口。声明端口是让 Kubernetes 知道容器对外提供服务的方式。
注意:即使声明了端口,从集群外部仍然无法直接访问 Pod IP(Pod IP 在容器网络内),需要通过 Service 暴露。
1.3 添加标签(1.3-labeled_pod.yaml)
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Pod
metadata:
name: mypod
labels:
app: webserver
spec:
containers:
- name: mycontainer
image: nginx:latest
ports:
- containerPort: 80
新增 labels.app: webserver。标签是键值对,用途:
标识资源属性(应用类型、层级、区域等)
被 Service 的 selector 用来匹配目标 Pod
被 kubectl get 的 -l 选项用来过滤资源
标签是 Kubernetes 中资源关联的核心机制。
1.4 资源请求与限制(1.4-resources_pod.yaml)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: v1
kind: Pod
metadata:
name: mypod
labels:
app: webserver
spec:
containers:
- name: mycontainer
image: nginx:latest
resources:
requests:
memory: "128Mi" # 128 MiB
cpu: "500m" # 0.5 CPU
limits:
memory: "128Mi"
cpu: "500m"
ports:
- containerPort: 80
新增 resources 字段:
requests:调度器据此选择节点,节点必须有足够的可分配资源
limits:容器运行时的资源上限,超过内存限制会被 OOMKilled
这里 requests = limits,QoS 等级为 Guaranteed(最不容易被驱逐)。不设置任何资源则为 BestEffort(最先被驱逐)。生产环境应始终设置资源请求。
2. Service
2.1 NodePort Service(2.1-web_service.yaml)
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
labels:
app: webserver
name: webserver
spec:
ports:
- port: 80
selector:
app: webserver
type: NodePort
selector.app: webserver — 匹配带有 app=webserver 标签的 Pod
ports.port: 80 — Service 端口,对应 Pod 的 containerPort
type: NodePort — 在每个节点上分配一个端口(30000–32767),集群外部可通过 节点IP:NodePort 访问
Service 解决的核心问题:Pod IP 不固定,Service 提供稳定入口并自动负载均衡。
3. 多容器 Pod 与命名空间
3.1 命名空间(3.1-namespace.yaml)
1
2
3
4
5
6
apiVersion: v1
kind: Namespace
metadata:
name: microservice
labels:
app: counter
命名空间用于隔离资源。不需要 spec,只需 name。使用 -n microservice 指定命名空间。
3.2 多容器 Pod(3.2-multi_container.yaml)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: redis
image: redis:latest
imagePullPolicy: IfNotPresent # 防止每次拉取 latest
ports:
- containerPort: 6379
- name: server
image: lrakai/microservices:server-v1
ports:
- containerPort: 8080
env:
- name: REDIS_URL
value: redis://localhost:6379 # 同 Pod 内用 localhost
- name: counter
image: lrakai/microservices:counter-v1
env:
- name: API_URL
value: http://localhost:8080
- name: poller
image: lrakai/microservices:poller-v1
env:
- name: API_URL
value: http://localhost:8080
4 个容器在同一个 Pod 中,共享网络栈:
Redis(数据层)监听 6379
Server(应用层)监听 8080,通过 localhost:6379 连接 Redis
Counter 和 Poller(支持层)通过 localhost:8080 连接 Server
imagePullPolicy: IfNotPresent 用于 latest 标签时防止每次都拉取。使用具体标签时默认就是 IfNotPresent。
局限性:Kubernetes 以 Pod 为最小扩缩单位,无法单独扩缩某个容器。如果需要独立扩缩,应拆分为多个 Pod + Service。
4. 服务发现(Service Discovery)
4.1 命名空间(4.1-namespace.yaml)
1
2
3
4
5
6
apiVersion: v1
kind: Namespace
metadata:
name: service-discovery
labels:
app: counter
为服务发现课程创建独立的命名空间 service-discovery,隔离本课资源。后续所有命令需要加 -n service-discovery。
4.2 数据层 — Pod + ClusterIP Service(4.2-data_tier.yaml)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Service
apiVersion: v1
kind: Service
metadata:
name: data-tier
labels:
app: microservices
spec:
ports:
- port: 6379
protocol: TCP
name: redis
selector:
tier: data
type: ClusterIP # 默认类型,仅集群内可访问
---
# Pod
apiVersion: v1
kind: Pod
metadata:
name: data-tier
labels:
app: microservices
tier: data # 被 Service selector 匹配
spec:
containers:
- name: redis
image: redis:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 6379
将 Redis 拆分为独立 Pod + ClusterIP Service。type: ClusterIP 是默认值,仅集群内部可访问。name: redis 为端口命名,后续可通过环境变量引用。
4.3 应用层 — 环境变量服务发现(4.3-app_tier.yaml)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
apiVersion: v1
kind: Service
metadata:
name: app-tier
labels:
app: microservices
spec:
ports:
- port: 8080
selector:
tier: app
---
apiVersion: v1
kind: Pod
metadata:
name: app-tier
labels:
app: microservices
tier: app
spec:
containers:
- name: server
image: lrakai/microservices:server-v1
ports:
- containerPort: 8080
env:
- name: REDIS_URL
# Environment variable service discovery
# Naming pattern:
# IP address: _SERVICE_HOST
# Port: _SERVICE_PORT
# Named Port: _SERVICE_PORT_
value: redis://$(DATA_TIER_SERVICE_HOST):$(DATA_TIER_SERVICE_PORT_REDIS)
# In multi-container example value was
# value: redis://localhost:6379
Kubernetes 自动为同命名空间的每个 Service 注入环境变量:
_SERVICE_HOST → Service 的 ClusterIP
_SERVICE_PORT → Service 的端口
_SERVICE_PORT_ → 命名端口
这里 DATA_TIER_SERVICE_HOST 和 DATA_TIER_SERVICE_PORT_REDIS 由 Kubernetes 自动注入,无需硬编码 IP。对比多容器 Pod 中的 redis://localhost:6379,现在通过 Service 跨 Pod 通信。
4.4 支持层 — DNS 服务发现(4.4-support_tier.yaml)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: v1
kind: Pod
metadata:
name: support-tier
labels:
app: microservices
tier: support
spec:
containers:
- name: counter
image: lrakai/microservices:counter-v1
env:
- name: API_URL
# DNS for service discovery
# Naming pattern:
# IP address: .
# Port: needs to be extracted from SRV DNS record
value: http://app-tier.service-discovery:8080
- name: poller
image: lrakai/microservices:poller-v1
env:
- name: API_URL
# omit namespace to only search in the same namespace
value: http://app-tier:$(APP_TIER_SERVICE_PORT)
两种服务发现方式对比:
DNS:. 或同命名空间内直接用
环境变量:$(_SERVICE_PORT) 等
counter 用了完整 DNS(app-tier.service-discovery:8080),poller 省略了命名空间并混合使用了环境变量。DNS 方式更灵活,推荐使用。
5. Deployment
5.2 数据层 Deployment(5.2-data_tier.yaml)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: apps/v1 # Deployment 使用 apps API 组
kind: Deployment
metadata:
name: data-tier
labels:
app: microservices
tier: data
spec:
replicas: 1 # 副本数
selector:
matchLabels:
tier: data # 必须与 template.metadata.labels 匹配
template: # Pod 模板
metadata:
labels:
app: microservices
tier: data
spec: # Pod spec
containers:
- name: redis
image: redis:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 6379
从裸 Pod 升级为 Deployment:
replicas:期望的 Pod 副本数
selector.matchLabels:Deployment 用来管理 Pod 的标签选择器
template:Pod 模板,Deployment 据此创建和管理 Pod
Deployment 提供滚动更新、回滚、自愈(Pod 挂了自动重建)
5.3 应用层 Deployment(5.3-app_tier.yaml)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
apiVersion: v1
kind: Service
metadata:
name: app-tier
labels:
app: microservices
spec:
ports:
- port: 8080
selector:
tier: app
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-tier
labels:
app: microservices
tier: app
spec:
replicas: 1
selector:
matchLabels:
tier: app
template:
metadata:
labels:
app: microservices
tier: app
spec:
containers:
- name: server
image: lrakai/microservices:server-v1
ports:
- containerPort: 8080
env:
- name: REDIS_URL
value: redis://$(DATA_TIER_SERVICE_HOST):$(DATA_TIER_SERVICE_PORT_REDIS)
与 4.3 相同的 Service + 环境变量服务发现,但 Pod 改为 Deployment 管理。
5.4 支持层 Deployment(5.4-support_tier.yaml)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
apiVersion: apps/v1
kind: Deployment
metadata:
name: support-tier
labels:
app: microservices
tier: support
spec:
replicas: 1
selector:
matchLabels:
tier: support
template:
metadata:
labels:
app: microservices
tier: support
spec:
containers:
- name: counter
image: lrakai/microservices:counter-v1
env:
- name: API_URL
value: http://app-tier.deployments:8080
- name: poller
image: lrakai/microservices:poller-v1
env:
- name: API_URL
value: http://app-tier:$(APP_TIER_SERVICE_PORT)
与 4.4 相同的 DNS 服务发现,但 Pod 改为 Deployment 管理。注意支持层没有 Service(不需要被其他组件访问)。DNS 中的命名空间从 service-discovery 变成了 deployments。
---
## 开源OpenSearch学习笔记|部署篇(二):Kubernetes 上用 Helm 快速搭建 OpenSearch.
- URL: https://blog.no-claw.com/posts/1e02510f/
- 日期: 2026-03-01
- 分类: Kubernetes
- 标签: Kubernetes
OpenSearch 是一个开源的分布式搜索和分析引擎,适用于日志分析、全文检索、应用监控等场景。本文将介绍如何使用 Helm 在 Kubernetes 集群上快速部署 OpenSearch 及 OpenSearch Dashboards,并通过 Secret 管理密码,避免将敏感信息硬编码在配置文件中。本文的 K8S 环境基于 Amazon EKS,部分步骤涉及 EBS CSI Driver 的配置,如果你使用其他 K8S 发行版可以跳过相关内容。
添加 Helm 仓库
官方提供了helm包,这使得我们可以很方便的下载OpenSearch的helm chart并且进行更新下载:
1
2
helm repo add opensearch https://opensearch-project.github.io/helm-charts/
helm repo update
创建 Secret 管理密码
安装文档上来说,我们需要在value.yml里添加我们的密码,不过在我看来这不是一个比较优雅的做法,于是我采取了使用secret的方式来管理密码,这样可以把密码和Helm Chart进行解耦。先创建名为opensearch-admin-secret 的secret,然后指定密码:
1
2
kubectl create secret generic opensearch-admin-secret \
--from-literal=OPENSEARCH_INITIAL_ADMIN_PASSWORD=''
安装 OpenSearch
然后再使用helm install安装opensearch,在这个时候把刚刚创建的secret传进去。
1
2
helm install opensearch opensearch/opensearch \
--set 'envFrom[0].secretRef.name=opensearch-admin-secret'
EKS 环境配置(非 EKS 用户可跳过)
由于我用的是亚马逊云科技的EKS,实际上还要处理关于CSI driver的问题。包括把gp2设置为默认的storageclass,以及使用 AmazonEBSCSIDriverPolicy这个策略,授权 EBS CSI Driver 操作 EBS 卷所需的 EC2 API 权限。
1
kubectl patch storageclass gp2 -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
主要包括:
ec2:CreateVolume / ec2:DeleteVolume — 创建和删除 EBS 卷
ec2:AttachVolume / ec2:DetachVolume — 挂载和卸载卷到 EC2 实例
ec2:DescribeVolumes / ec2:DescribeInstances — 查询卷和实例信息
ec2:CreateSnapshot / ec2:DeleteSnapshot — 快照操作
ec2:DescribeAvailabilityZones — 查询 AZ 信息
不过需要注意的是,通常来讲,在EKS上需要创建iam-oidc-provider 以及 iamserviceaccount来关联这个策略,以此来获得操作AWS API的权利。不过为了防止文章更加偏向于EKS,我修改了 hop limit,这是让EKS 的pod 可以像普通K8S一样访问host的资源。生产环境推荐使用 IRSA(IAM Roles for Service Accounts)通过 OIDC provider 关联 IAM 策略,这是 EKS 的最佳实践。本文为了简化流程,采用了修改 hop limit 的方式。
1
2
3
4
5
# 获取每个node的 instance ID 然后修改
aws ec2 modify-instance-metadata-options \
--instance-id \
--http-put-response-hop-limit 2 \
--region
简单解释下:Pod 在容器网络里,经过 veth pair 到宿主机网络再到 IMDS,比实例本身多了一跳。所以 hop limit 为 1 时,Pod 里的 EBS CSI controller 无法访问 IMDS,拿不到 IAM 凭证,所有 AWS API 调用都失败。改成 2 就是允许多一跳,让 Pod 也能访问 IMDS。当然,如果你用的是其他K8S的发行版那么就可以跳过这个问题。
重建与缩容
修改之后我们删除Helm Chart并且重建:
1
2
3
4
helm uninstall opensearch
kubectl delete pvc -l app.kubernetes.io/name=opensearch
helm install opensearch opensearch/opensearch \
--set 'envFrom[0].secretRef.name=opensearch-admin-secret'
如果需要更新,那么可以使用helm upgrade 来修改参数,helm默认启动了3个master节点,而我只有两个node,为了合理的分配一下资源,我决定对pod数量进行缩容。
1
2
3
helm upgrade opensearch opensearch/opensearch \
--set replicas=2 \
--set 'envFrom[0].secretRef.name=opensearch-admin-secret'
验证与测试
这样我们多节点的OpenSearch Cluster就启动好了,当然服务在pod里,我们如果想本地测试(因为EKS控制平面是托管的,我无法登陆),所以使用kubectl port-forward来进行转发,这样就可以把端口代理到本地。
1
2
kubectl port-forward svc/opensearch-cluster-master 9200:9200 &
curl -sk https://localhost:9200/_cluster/health\?pretty -u 'admin:'
安装 OpenSearch Dashboards
当然opensearch-dashboards也可以如法炮制进行安装:
1
2
3
4
5
6
# helm 安装opensearch-dashboards
helm install dashboards opensearch/opensearch-dashboards \
--set opensearchHosts="https://opensearch-cluster-master:9200"
# 端口转发
kubectl port-forward svc/dashboards-opensearch-dashboards 5601:5601
然后通过浏览器打开 http://localhost:5601 登录,能够看到我们刚刚通过helm创建的两个Pod。
总结
到这里,我们已经通过 Helm 在 Kubernetes 上成功部署了 OpenSearch 集群和 Dashboards。整个过程中我们用 Secret 解耦了密码管理,用 Helm upgrade 灵活调整了副本数。如果你想进一步探索,可以尝试配置 Ingress 暴露服务、接入 Fluent Bit 采集日志,或者通过 Index State Management 管理索引生命周期。希望这篇文章对你有帮助。
---
## Easysearch 数据映射之 Deep Dive:我踩过的 Volume 坑
- URL: https://blog.no-claw.com/posts/65e472bd/
- 日期: 2026-02-25
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
最近在用 Docker 部署 Easysearch,本以为是个简单的事情,结果在数据持久化上栽了跟头,每次停止再启动容器之后都会503,在后面成了我百思不得其解的问题,后来一直在某次的meetup中,请教了原厂的罗老师,一句话点醒梦中人,Easysearch用的具名卷,防止宿主机的数据覆盖容器里的数据。
数据映射的尝试
volume 和 bind 我就纠结了好久,以前习惯使用的是bind的方式。
1
2
3
4
5
docker run -d \
-v ./node1/data:/app/easysearch/data \
-v ./node1/logs:/app/easysearch/logs \
-v ./node1/config:/app/easysearch/config \
infinilabs/easysearch:2.0.2-2499
然后… 起不来。
1
docker logs easysearch-node1
日志里提示 JVM 配置文件找不到,服务启动失败返回 503。原因:宿主机的 ./node1/config 是空目录,Bind Mount 把它挂进去后,直接遮盖了容器内原有的 JVM 配置和默认配置文件。
老老实实按官方文档用 Named Volume:
1
2
3
4
5
docker run -d \
-v es-data1:/app/easysearch/data \
-v es-logs1:/app/easysearch/logs \
-v es-config1:/app/easysearch/config \
infinilabs/easysearch:2.0.2-2499
Named Volume 挂载到容器内非空目录时,会自动把容器内的文件复制到卷里,包括 JVM 配置、默认配置等。集群顺利起来了:
1
2
3
4
5
curl -ku admin:admin https://localhost:9201/_cat/nodes?v
ip heap.percent ram.percent cpu load_1m load_5m load_15m node.role master name
172.24.0.3 68 31 31 1.67 0.57 0.21 dimr - easysearch-node1
172.24.0.2 55 31 31 1.67 0.57 0.21 dimr * easysearch-node2
为什么 Easysearch 要用 Named Volume?
Easysearch 镜像内自带完整的默认配置:JVM 参数、节点配置、安全证书等。这些文件在容器的 /app/easysearch/config 目录里。
Named Volume:空卷挂载时,Docker 会把容器内的文件复制到卷里,JVM 配置等默认文件保留
Bind Mount:宿主机目录直接遮盖容器内文件,空目录挂进去 = 配置全丢
除非你用 init.sh 之类的脚本在宿主机预先生成了所有配置文件(包括 JVM 配置),否则不要用 Bind Mount 挂 config 目录。
附:Bind Mount vs Volume 核心区别
特性
Bind Mount
Named Volume
容器内原有文件
❌ 被遮盖
✅ 空卷时自动复制
宿主机直接编辑
✅
❌
适合场景
宿主机已准备好文件
容器内自带默认配置
关键行为差异:
Bind Mount(Docker 官方文档):
如果你把宿主机的文件或目录 bind mount 到容器内一个已有文件的目录,容器内原有的文件会被遮盖。
Volume(Docker 官方文档):
如果你启动容器时创建了一个新卷,而容器内挂载目录(如 /app/)已有文件,Docker 会把这些文件复制到卷里。
Easysearch 这种容器内自带 JVM 配置和默认配置的场景,Named Volume 才是正确选择。Bind Mount 空目录会把这些文件全遮盖掉,别像我一样自作聪明。
参考
Easysearch Docker 部署文档
Docker 官方文档 - Volumes
Docker 官方文档 - Bind Mounts
---
## 懒猫微服实战入门(三十六):懒猫微服QEMU虚拟机快速上手
- URL: https://blog.no-claw.com/posts/5f202ca3/
- 日期: 2026-02-25
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服
对于 NAS 玩家来说,虚拟机绝对是标配。今天我们要介绍的主角是 QEMU。你可能会觉得它太过底层、全命令行操作太硬核,但别担心,看过这篇文章之后,你就能轻松在懒猫微服上操作它。
在传统 Linux 下装 QEMU,你可能要折腾一堆 kvm-ok 检测、各种动态库依赖。但在懒猫微服上,直接从商店下载即可。这就是全容器化的好处:环境全封闭,不会把宿主机的依赖搞坏,不用再和底层依赖打交道,这就是懒猫微服全容器化的好处,彻底解决了让人头疼的环境问题。
和其他虚拟机一样,我们需要一个 ISO 镜像。QEMU 厉害的地方在于它对镜像来源几乎从不挑剔
可以直接填入官网的 ISO 下载链接,边下边装,省去中间转手的麻烦。
如果你的镜像在电脑里,或者在 NAS 上,那么可以直接在存放镜像的文件夹下打开终端,用一行 Python 命令把它变成“下载站”:
1
2
# 进入镜像文件夹执行,默认开启 8000 端口
python -m http.server 8000
然后在 QEMU 的镜像地址里填入:http://你的电脑IP:8000/ubuntu-22.04.iso。
在懒猫微服的界面上,你只需要选好分给虚拟机的 CPU 核心数和内存大小。
如果你想稍微硬核一点,看看后台它是怎么运行的,其实 QEMU 常见的“加速”命令已经默默为你打好了。比如:
-m 2G:给虚拟机分配 2GB 内存。
-smp 4:给虚拟机 4 核 CPU。
当然你可以对这些参数进行修改,完成可以改成4C8G或者更高的配置。
当然也可以安装安卓系统,我尝试运行了BlissOS,很流畅,除了必要的X86和ARM指令集转换缺失之外,其他的都可以流程运行。甚至把虚拟机内部的 5555 端口(ADB 默认调试端口)通过端口映射的工具映射出来。这样,我们在局域网内的任何一台电脑上,只需要执行这个命令就可以进行ADB调试:
1
adb connect 你的NAS的IP:你映射的端口号
进行端口转发之后,就可以像操作真机一样,直接用 adb install 往虚拟机里塞 APK,或者在线的调试。
安装完成之后,完全不用担心远程连接的问题,甚至还自带一个web的no-VNC。
好了,安装完了,咱们从纯技术的角度聊聊 QEMU,以及它和 KVM 到底是什么关系。
QEMU:全能模拟器
QEMU 是一个纯软件实现的虚拟机。它的强大之处在于“无所不拟”:它可以在 x86 架构(普通电脑)上模拟出 ARM、MIPS 甚至 PowerPC 的环境。
代价: 这种“纯软件模拟”就像是找了个翻译官,每条指令都要翻译一次才能执行,所以单跑 QEMU 的速度比较慢,跑起来像在泥潭里走路。
KVM:内核加速器
KVM(Kernel-based Virtual Machine)是 Linux 内核的一个模块。它的作用是让虚拟机直接调用 CPU 的硬件虚拟化指令(如 Intel VT-x)。
优点: 它的性能极快,几乎能发挥出硬件的真实水平。
局限: 但 KVM 很“高冷”,它只管 CPU 和内存,至于怎么模拟显卡、鼠标、USB 接口,它一概不管。
在懒猫微服里,我们默认用的是 QEMU-KVM 模式:
KVM 负责干体力活: 接管 CPU 和内存,保证运行速度飞快。
QEMU 负责干技术活: 模拟所有的外设(显卡、网卡、声卡、USB 驱动等),并提供管理功能。
总结:QEMU 是大脑和管家,KVM 是强壮的肌肉。两者是黄金搭档,才有了我们在懒猫微服上流畅的虚拟机体验。
---
## 超效率手册
- URL: https://blog.no-claw.com/posts/b6e1e84a/
- 日期: 2026-02-24
- 分类: 摘抄
周/日 计划
限制时间做事(30-90min) 紧迫感、
任务分解
短跑。起床坚持10分钟,其他事再坚持20分钟休息。 短跑30天习惯
日程校对,相信自己要做的事情。完全日程,避免过度工作和拖延。
自律。停下来之前再坚持10分钟。下次做事再坚持10-20%,专注一件事
语录刺激生产效率(便利贴
目标还是否能激励?有没有把生产力当作借口?
目标列出来看,承诺,便利贴看见
收集想法再整理剔除
尽可能的减少干扰
想要做/不去做。拒绝应该做。
先做收到反馈再调整
占据你时间但是价值的事情 根除或者压缩时间
事情条理性,定期维护
任务和物品整理好固定位置。(少混乱,少压力)
随时把想法记录下来(捕捉灵感
所有物品规定地方(和16差不读
GTD组织系统(项目-任务-活动)
项目:大类
任务:小的独立的行动。(总体待办/周目标/日目标)
活动:特定时间干的事(避免周日历太多)
写下目标,1. 不会遗忘。2. 有动力实现 3.模糊的想法变成具体目标
没有完美的系统就是设置分支,多文件夹 或者 决策的if else
通信记录承诺的事(日期时间 谁 内容 联系方式)
承诺的事整理起来不要忘记 设置时间线
有条理 减少检索时间
阅读笔记 - 提取关键信息
数字资料分类调理
晨礼(30-60分固定流程,锻炼,早晨应该避免过度脑力劳动,决定而第二天起床时间
每周给自己放一天假缓冲(上6休1) 剩下一天无所事事
有限的是精力不是时间,尝试早起20分钟慢跑
吃的少油少肉,多吃蔬菜和主食。少食多餐
吃掉那只青蛙。每天重要的事放在晨礼之后,每周重要的事放在周一周二
能量周期循环(休息-回复) 工作看成一系列短跑。每周一天放松不做生产力,每天晚上固定时间不想工作的事。
开环闭环理论,任务有明显的结束信号。
尽可能不要多进程处理,会让人变笨。
停止之前再坚持15分钟。
多喝水。杯子接满水。
回顾目标来充电。
换掉不能提供新想法的信息源。进行脑力训练。
工作 - 休息的循环。而不是放任自流。这只会变得懒惰。
不同的活动不同思维,累了就换一个。体能低就平衡创造力
完成项目不是任务,因为任务是动态的。
任务和项目的最后期限
每周90分钟回顾。独立思考,隔离其他的干扰源。
硬时间之前自己加一个软时间。(硬时间会积压一起)
帕金森定律:只要有时间,工作就会不断拓展,直到把时间用满。
霍夫施塔定律:在一件事情上花费的时间总是比想象中多得多。
生产力 不等于 工作。 简化复杂的项目,不做没有价值的大任务。断舍离用头脑挽救时间精力。
框架规划。只规划出来绝对必要的要素就可以了。不要过度规划,灵活决策后面做。
不要重复造轮子。用很少的钱换取别人几百个小时的劳动。
快速MVP迭代(找出版社之前先写一本电子书)
别按照时间会的报酬。(2-3小时干完,剩下时间休息
被逼出来的生产力(加任务/先休假)
每日检查标记
出口策略:完成工作之后做什么
---
## 给Macbook Pro安装Fedora Asahi Remix
- URL: https://blog.no-claw.com/posts/5743bae5/
- 日期: 2026-02-22
- 分类: 电子产品 / 电脑 / 苹果
- 标签: Apple
16G的Macbook 经常出问题,应该是内存太小吧,所以安装一个Linux做双系统吧,Asahi搞定UEFI这层,本质上还是Fedora。
起码目前阶段对我的M2 Macbook 兼容还不错,打算尝尝鲜。
没找到图形化安装或者ISO,只找到这个命令来做安装。
1
curl https://alx.sh | sh
纯CLI的安装方式其实没那么太友好,一开始显示还有200多G的空间但是
查看本地快照
在终端中运行:
1
tmutil listlocalsnapshots /
删除所有本地快照
方法 A(推荐): 批量删除所有快照
1
tmutil listlocalsnapshotdates | grep "-" | xargs -n1 sudo tmutil deletelocalsnapshots
方法 B: 逐个删除(如果方法 A 无效)
1
sudo tmutil deletelocalsnapshots 2024-01-15-123456
验证清理结果
1
tmutil listlocalsnapshots /
应该显示为空或 “No local snapshots on this date”。
然后就是压缩MacOS磁盘给一个部分到这个Fedora,压缩磁盘的时候MacOS页面会卡死一阵子。
然后就是选容量和系统类型。剩下就是漫长的等待。
然后会把第一引导项改成Fedora。
第一次安装:中间断网了,重装
第二次安装:无限重启重启(因为手贱按了两次开机键,实际长按看见引导就行)
第三次成功(要安装之后关机25秒后再开)
剩下就是无脑跟着提示走,安全模式设置,然后就是初始化了。
嗯,Retina屏幕看啥都好看,在MacOS越做越烂的这些年,换个Linux玩玩吧。
---
## 安卓ADB提取APK安装包
- URL: https://blog.no-claw.com/posts/17adb539/
- 日期: 2026-02-21
- 分类: 电子产品 / 软件技巧
- 标签: 手机
之前的文章写了如何使用无线调试ADB,那么我们就可以用adb提取安装包了。
1
adb shell pm list packages
这里会列出很多app的包名,如果你知道叫啥名字页可以grep过滤一下。
然后,可以用这个命令看包名的地址。会给一个地址
1
adb shell pm path <包名>
然后使用adb pull这个地址,就可以在当前路径得到apk。
1
adb pull <地址>
---
## 记一次硬盘满了导致 Coco Server 无法启动的排查
- URL: https://blog.no-claw.com/posts/db853650/
- 日期: 2026-02-20
- 分类: 极限科技 / Coco
- 标签: Coco-AI, 极限科技
这次排查非常典型:容器日志看起来像“卡在某一行”,Easysearch 甚至已经启动,但 Coco Server 的进程并没有真正跑起来。
最后发现根因是:磁盘剩余空间不足(< 5GB)触发 coco 自检阈值,直接 panic 退出,造成了“日志一直卡住、服务一直起不来”的假象,差不多排查了半个多小时。
背景:目标与现象
目标是在 RK3566 / Armbian 上把 cocoai-arm:test 跑起来,并通过宿主机端口访问服务:
宿主机映射:-p 9000:9000
容器挂载 volume:
coco_data_vol:/app/easysearch/data
coco_config_vol:/app/easysearch/config
coco_logs_vol:/app/easysearch/logs
一开始看到的现象是:
docker logs -f 输出大量初始化日志
Easysearch 选主、集群状态、模板 / 索引迁移都能看到
但实际访问服务不通,或者看上去“卡在某一行不动”
Step 1:先处理内核参数(vm.max_map_count)
Easysearch / ES 系产品常见依赖 vm.max_map_count,先按推荐值设置:
1
2
3
sysctl -w vm.max_map_count=262144
echo "vm.max_map_count=262144" >/etc/sysctl.d/99-easysearch.conf
sysctl --system | grep vm.max_map_count
确认 vm.max_map_count = 262144 已生效。
日志里还会看到一条:
1
sysctl: setting key "net.ipv4.conf.all.promote_secondaries": Invalid argument
这是系统 / 内核不支持该 sysctl 键导致的“噪音”,不影响 vm.max_map_count 是否生效。只要 grep 结果正确,就可以继续下一步。
Step 2:确认 entrypoint / 进程结构(关键转折)
这一步是整个排查的关键转折点:
不要只盯日志,要看容器里到底跑了哪些进程。
先看镜像入口:
1
docker inspect --format 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}' cocoai-arm:test
输出类似:
Entrypoint:["/sbin/tini","--","/sbin/entrypoint.sh"]
Cmd:["easysearch"]
接着进容器看进程结构:
1
docker exec -it cocoserver sh -lc 'ps -ef'
可以看到非常关键的进程关系:
PID 1:tini → entrypoint.sh easysearch
PID 7:java(Easysearch)
PID 147:python supervisord
PID 581:/bin/bash /app/easysearch/data/coco/start-coco.sh
这一步直接改变了排查方向:
容器里不仅跑了 Easysearch,还跑了 supervisord,它负责拉起 coco。
所以“服务用不了”,问题很可能根本不在 Easysearch,而在 coco。
Step 3:端口检查发现 Easysearch 只监听 127.0.0.1:9200
继续验证服务监听情况:
1
2
docker exec -it cocoserver sh -lc \
"ss -lntp | grep -E ':9000|:9200' || netstat -lntp | grep -E ':9000|:9200' || true"
容器里没有 ss,回退到 netstat,看到类似:
1
tcp6 0 0 127.0.0.1:9200 :::* LISTEN -
解读:
Easysearch 在容器内监听 127.0.0.1:9200(loopback)
容器内部组件访问 127.0.0.1:9200 是没问题的
但如果你想从宿主机直接访问 9200,那一定不行(而且你也没映射 9200)
coco server 在9000 端口到底有没有起来?外部访问返回RST。
Step 4:抓到致命错误:磁盘空间不足导致 coco panic 退出
把视角切到 coco 的 supervisor 日志(这是最关键的一步):
1
2
docker exec -it cocoserver sh -lc \
"tail -n 120 /app/easysearch/data/coco/supervisor.out.log; echo '----'; tail -n 120 /app/easysearch/data/coco/supervisor.err.log || true"
日志里可以看到两条定性信息:
1
api server listen at: http://0.0.0.0:2900
以及真正的致命错误:
1
2
[app.go:407] panic: disk free space [3.7G] < threshold [5G]
[app.go:429] coco now terminated.
到这里就完全对上了:
1)coco 确实尝试启动
2)启动后立即做磁盘空间自检
3)可用空间 3.7G < 5G 阈值
4)直接 panic 退出
5)日志不再输出,看起来像“卡住”
这次排查最后可以一句话总结为:
Easysearch 启动正常、集群 Green;真正导致 Coco Server 不可用的原因是:
coco 因为磁盘可用空间不足(< 5GB)触发保护阈值直接 panic 退出,造成“像卡住”的假象。
删除了一堆Docker images 释放了磁盘空间,Coco Server就能顺利启动了~
下次直接照抄的「快速定位清单」
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1) 容器状态
docker ps -a | grep coco
# 2) 镜像入口:确认是否存在 supervisord / 多进程
docker inspect --format 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}' cocoai-arm:test
# 3) 进程树:确认 coco 是否被 supervisor 拉起
docker exec -it cocoserver sh -lc 'ps -ef'
# 4) 监听端口:没 ss 就用 netstat
docker exec -it cocoserver sh -lc "netstat -lntp | grep -E ':9200|:2900|:9000' || true"
# 5) coco 的失败原因:优先看 supervisor.out.log
docker exec -it cocoserver sh -lc "tail -n 120 /app/easysearch/data/coco/supervisor.out.log"
# 6) 如果看到 disk threshold,立即在宿主机查空间
df -h
---
## 安卓也有terminal,使用无线ADB解锁X300的终端
- URL: https://blog.no-claw.com/posts/d5ad04d5/
- 日期: 2026-02-19
- 分类: 电子产品 / 软件技巧
- 标签: 手机, ADB
刷视频的时候看见有人说X300自带了一个终端,所以打算复现一下。
我的手机上安装甲壳虫ADB一直闪退,所以使用无线ADB,开启之后效果大概这样子。
无线ADB
需要先打开开发者模式,然后在Mac上安装ADB。
1
2
brew install android-platform-tools
adb version
X300的ADB用的不是常用的5555端口,所以需要在这里找到配对码和配对端口(port1)。
大改就是先 adb pair 然后再 adb connect ,需要注意的是port1和port2是俩不同的端口。
Port1: 配对端口
Port2: 连接端口
1
2
3
4
5
6
7
8
9
10
(base) ➜ ~ adb pair ip:port1
Enter pairing code: xxxxx
Successfully paired to ip:port [guid=adb-10AG1G06C9005PH-m5M0Ww]
(base) ➜ ~ adb connect ip:port2
connected to 192.168.5.25:40779
(base) ➜ ~ adb devices
List of devices attached
ip:port2 device
然后adb查看手机软件:
1
2
3
4
adb -s ip:port shell pm list packages | grep terminal
package:com.android.virtualization.terminal
解锁终端
然后使用这个命令解锁终端,看到enabled之后就可以使用手机查看了。
1
2
adb -s ip:port shell pm enable com.android.virtualization.terminal
Package com.android.virtualization.terminal new state: enabled
在手机里看操作系统信息,竟然是debian,不过看起来是纯内网使用。
可以调整磁盘大小。
还可以调整端口。
从这里看好像是一个没有不联网的Linux虚拟机,不过崩溃的频率还是有点高,有条件还是自己弄VPS尝鲜吧。
---
## RK3566嵌入式开发板运行Coco AI Sever
- URL: https://blog.no-claw.com/posts/ce955007/
- 日期: 2026-02-17
- 分类: 极限科技 / Coco
- 标签: Coco-AI, 极限科技
之前在泰山派上运行了Easysearch,这次也想着是不是可以在泰山派开发板RK3566上运行Coco server,毕竟这板子功耗小,适合常开。
我的RK3566上是Armbian,但是没有配置网络环境,访问Dockerhub有问题,所以从Macbook 上下载玩,然后通过离线方式导成tar文件。我的镜像改了tag叫做cocoai-arm:test,你也可以不改。
1
docker save -o cocoai-arm_test.tar cocoai-arm:test
然后通过SCP上传到RK3566的开发板
1
2
3
4
5
6
7
scp cocoai-arm_test.tar lckfb@192.168.5.36:~
** WARNING: connection is not using a post-quantum key exchange algorithm.
** This session may be vulnerable to "store now, decrypt later" attacks.
** The server may need to be upgraded. See https://openssh.com/pq.html
lckfb@192.168.5.36's password:
cocoai-arm_test.tar 14% 125MB 5.6MB/s 02:14 ETA^cocoai-arm_test.tar 16% 143MB 5.7MB/s 02:10 ETA^cocoai-arm_test.tar 38% 337MB 5.7MB/s 01:35 ETA
cocoai-arm_test.tar 44% 391MB 5.5MB/s 01:29 ETA
补充:这个 warning 主要是 SSH 协商算法提示,不影响传输本身。真正要关注的是传输速度和是否中断——毕竟 900MB 级别的镜像,板子这边 IO 慢一点就容易“感觉很久”。
然后ssh到开发板上,使用docker laod命令还原这个Docker镜像。
1
2
root@lckfb:/home/lckfb# docker load -i cocoai-arm_test.tar
45e40363867d: Loading layer 336.5MB/922.7MB
这个过程会花费一些时间,所以有时候假死直接等待就好了。漫长的时间过去之后,我们可以通过Docker images来查看镜像,还有一个Easysearch 镜像,是我之前测试的,也能够在嵌入式开发板上运行的很好。
1
2
3
4
root@lckfb:/home/lckfb# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
cocoai-arm test 172b428f2dcf 45 hours ago 915MB
infinilabs/easysearch 1.15.0 295014c1f959 5 months ago 697MB
docker load “假死”一般是写盘/解包慢,我这个板子的 eMMC 速度就很一般。
顺手可以记一下:板子上镜像多了以后,存储空间会很快见底,后续最好固定一个清理策略(比如只留当前版本)。
然后使用这个命令启动,然后使用Docker PS查看。
1
2
3
4
5
docker run -d --name cocoserver -p 9000:9000 \
-v coco_data_vol:/app/easysearch/data \
-v coco_config_vol:/app/easysearch/config \
-v coco_logs_vol:/app/easysearch/logs \
cocoai-arm:test
图:容器已经跑起来,端口映射也生效。
我当时会顺手确认几个问题:
STATUS 是不是一直 Up,有没有出现反复重启(Restarting)?
PORTS 是否显示 0.0.0.0:9000->9000/tcp(如果只绑到 127.0.0.1,局域网其他设备就访问不到)
容器名 cocoserver 固定后,后续排查日志/重启都更方便:docker logs -f cocoserver
开发板的性能有限,所以初始化的时候需要多等一会。
图:初始化日志在跑,说明服务在“慢慢起身”。
这张图我会重点盯两类信息:
有没有明显的报错关键词:error / exception / failed / oom / killed
有没有持续输出(哪怕很慢)——只要日志还在动,一般就不是卡死
补充一个小习惯: 如果你怀疑卡住了,可以开另一个窗口 docker stats 看 CPU/内存是否还有波动;也可以 docker logs --tail 200 cocoserver 快速看最近输出。
漫长的等待之后,CPU很清闲,不过内存快用满了。
补充: 这种“CPU 很闲、内存紧张”的状态,在小板跑 Java/搜索组件/AI 服务时很常见,能跑起来不奇怪,关键是别被 OOM 一刀带走。
到这里,Coco AI Server 算是成功在 RK3566 上跑起来了:容器状态正常、日志能持续输出、端口映射也能对外提供访问。
---
## 被CUPS共享打印机驱动搞疯了,换windows10 做打印机 server吧
- URL: https://blog.no-claw.com/posts/7cc2eba9/
- 日期: 2026-02-17
- 分类: 电子产品 / 打印机
- 标签: 外设
之前黑群晖上部署的CUPS挂了,后来查了一下是数据盘坏了,还有一个原因是CUPS对联想打印机的兼容不是很好,索性就刷成windows10,有原生驱动,出问题还能RDP。
参考了这个方案:
兜兜转转回到了一个不折腾的方案:Win10 安装 Mobility Print Server → 发布本地打印机 → 手机走 AirPrint / Mobility Print 直接打印。
其实就是:
让 Win10 这台电脑充当“打印服务器”,把 USB/本地打印机发布到局域网里,手机就能像用无线打印机一样用它。
如果你家里有打印机可以试试,比CUPS的安装和运维成本小很多。
windows 安装打印机驱动
先把打印机的 Windows 驱动和配套软件装好(重点是:确保 Win10 上“本地打印”完全正常)。
驱动下载页(示例):
https://www.lenovoimage.com/index.php/services/servers_drivers?cat_id=2&ProCode=48001877&OS=%E5%85%A8&baseclass=&key_words=
有种折腾半生,安装Windows养老的感觉。
可以从这里看到了我安装的软件列表。Windows自带的打印机共享好像是SMB,总之是不太好用,我直接给他关掉了。
这个办法算是解决技术债,有条件还是建议买带无线打印的机器。
条件确认
打印机在 Win10 上 能正常本地打印
Win10 这台电脑后续要当“打印服务器”
不要睡眠/休眠(至少在你要打印的时间段)
手机和这台电脑要在同一个 Wi-Fi/同一网段(不要访客网络)
这里最容易翻车的是两件事:
1)Win10 睡死
很多人以为“屏幕熄灭”没关系,但如果机器进入睡眠/休眠,手机就会直接找不到打印机。
2)访客 Wi-Fi / 网络隔离
有些路由器默认把“访客网络”做了隔离,手机在访客 Wi-Fi 下是看不到你内网 Win10 电脑的——打印服务自然也发现不了。
让GPT写了一个命令,目测还挺稳的:让这台 Win10 在你需要打印时别睡过去(屏幕可以灭,但系统别休眠/睡眠)。
1
2
3
4
5
6
7
powercfg -h off
powercfg /x standby-timeout-ac 0
powercfg /x standby-timeout-dc 0
powercfg /x hibernate-timeout-ac 0
powercfg /x hibernate-timeout-dc 0
powercfg /x monitor-timeout-ac 10
powercfg /x monitor-timeout-dc 10
稍微解释一下这几行大概在干嘛(不用深究,但有助于排错):
powercfg -h off:关掉休眠(顺带会禁用“快速启动”相关的一些行为)
standby-timeout-* 0:不进入睡眠
hibernate-timeout-* 0:不进入休眠
monitor-timeout-* 10:屏幕 10 分钟后熄灭(省电,但不影响打印服务)
Win10 安装 Mobility Print Server 并发布打印机
在 Win10 上安装 Mobility Print Server(安装过程基本一路下一步即可),不需要额外安装组件
打开 Mobility Print Server 管理界面,在打印机列表里 勾选“发布/共享” 你的那台打印机,中间简单配置下用户名米啊么
例如:Lenovo M7400 Pro(或者你自定义的队列名)
给这台打印机设置一个共享显示名称
发布后,你会在界面里看到打印机处于可用状态,并且共享名称生效。
iPhone 打印
iPhone 这边基本是“傻瓜式”:
只要手机和 Win10 在同一网络里,你在任意支持打印的 App(备忘录、照片、文件)里点“打印”,就能看到你发布出来的打印机名称,选中即可打印。
也就是说:iPhone 端通常不需要额外装 App,体验非常接近原生 AirPrint。
安卓打印
安卓这边装一个 Mobility Print(App)就行。装好后同样确保在同一 Wi-Fi/同网段,然后在打印选择里找到你刚才设置的共享显示名称,直接打印。
如果你家里安卓机比较多,这个方案的好处是:一次配置,全家通用,后续基本就是“选打印机 → 打印”。
实际效果
把 Win10 当打印服务器之后,手机端看到的体验就是:打印机像一台“真正的无线打印机”一样出现。
下面两张图就是我这边的实际效果(手机端能直接发现、直接选择、直接打印):
后记
用熟悉的Windows做Server,出问题还能RDP,有window原厂驱动还比CUPS少折腾了很多。也不需要买任何“无线打印盒子”,只要家里有一台 Win10 电脑能长期在线即可。
关键点就两个:
Win10 必须能稳定在线(别睡死)
手机和 Win10 必须在同一网络(访客 Wi-Fi 很容易把设备隔离掉)
当然了,这个windows还能当远程小主机用,安装微信,Office远程办公也行啊。
https://sspai.com/post/63776
Bonjour Print Services (Windows)
AirPrint Installer
修改防火墙
其中AirPrint Installer对我没有起到什么明显的作用,只是可以在移动端搜到打印机了,但是每次都打印失败。
Bonjour 还留着了,也不确定是不是真的有用,现在能用就不折腾了。
---
## Docker启动Coco AI Server后,如何访问内置Easysearch
- URL: https://blog.no-claw.com/posts/b7b40656/
- 日期: 2026-02-16
- 分类: 极限科技 / Coco
- 标签: Coco-AI, 极限科技
使用 Docker 启动 Coco AI 的时候会自带一个 Easysearch,我们使用连接器连接外部数据源的时候,就会把这个数据解析到 Easysearch 里。
但是默认的话,容器不会把这个 Easysearch 的端口映射出来,那就需要我们自己做些小的技巧:在官网的命令上修改一下,把 9200 端口先映射出来。
启动命令如下:
1
2
3
4
5
6
7
docker run -d --name cocoserver \
-p 9000:9000 \
-p 9200:9200 \
-v coco_data_vol:/app/easysearch/data \
-v coco_config_vol:/app/easysearch/config \
-v coco_logs_vol:/app/easysearch/logs \
infinilabs/coco:0.10.0-2678
启动之后,可以使用 netstat 看到容器端口的情况。换句话说,这个自带的 Easysearch 把 9200 和 9300 的端口确实启动起来了,但默认只绑定在 127.0.0.1 上,所以外部访问不到——即使你已经加了 -p 9200 也不行。
1
docker exec -it cocoserver sh -lc "netstat -lnt | egrep ':9000|:9200|:9300' || true"
1
2
3
tcp6 0 0 127.0.0.1:9200 :::* LISTEN
tcp6 0 0 127.0.0.1:9300 :::* LISTEN
tcp6 0 0 :::9000 :::* LISTEN
因为这部分是 easysearch.yml 控制的,所以我们可以直接通过命令更改,就使用 sed 替换吧。使用之前最好确认 Coco Server 进程已经起来了,防止不必要的问题。
1
2
3
4
5
6
docker exec -it cocoserver bash -lc '
CFG=/app/easysearch/config/easysearch.yml
cp -a "$CFG" "$CFG.bak.$(date +%Y%m%d%H%M%S)"
sed -i "s/^network\.host:\s*127\.0\.0\.1/network.host: 0.0.0.0/" "$CFG"
grep -n "^network\.host:" "$CFG"
'
然后重启服务,就能通过 https://IP:9200 的方式访问 cocoserver 自带的 Easysearch 了。
1
docker restart cocoserver
这个时候我们再看端口的占用情况,已经开了外网访问(9200/9300 不再是 127.0.0.1,而是对外监听了)。
1
(base) lzcbox-029c588e ~ # docker exec -it cocoserver sh -lc "netstat -lnt | egrep ':9000|:9200|:9300' || true"
1
2
3
tcp6 0 0 :::9300 :::* LISTEN
tcp6 0 0 :::9200 :::* LISTEN
tcp6 0 0 :::9000 :::* LISTEN
Easysearch 的进程会先起来,Coco Sever 会慢一些。这时候可以通过 http://ip:9000 访问 Coco Server,也可以通过 https://IP:9200 访问 Easysearch 和 Easysearch 的 UI。密码还是老样子去 log 里找。
可以直接从 log 里把 curl 示例过滤出来,一般会直接告诉你默认账号密码:
1
pg-docker logs cocoserver 2>&1 | egrep -i "curl" | tail -n 50
输出里会有类似这一行(账号通常是 admin,密码就是那串 Coco-Server-xxxx==):
1
curl -ku 'admin:Coco-Server-6dsxtGXhD+MUNhPBKf1hug==' https://localhost:9200
如果你是从宿主机访问,把 localhost 换成你的 IP 或 127.0.0.1 就行:
1
curl -k -u 'admin:Coco-Server-6dsxtGXhD+MUNhPBKf1hug==' https://127.0.0.1:9200
然后我们就收获了一台可以在 Coco server 里使用的 Easysearch,可以实时查看数据。
平时用 API 写进去的数据还能被 Coco Server 索引,自带 UI 真的太爽了!
到这里就搞定了:Coco Server 自带的 Easysearch 不仅能正常跑起来,还能把 9200 端口暴露出来给外部访问。
日常用法也很简单:
连接器同步进来的数据会实时写进这台 Easysearch 里;
自己用 API 写进去的数据也会被 Coco Server 索引;
遇到“数据到底进没进、字段长啥样、索引有没有建对”这种问题,直接打开 Easysearch UI 看一眼就能确认,再也不用抓瞎!
---
## 懒猫微服实战入门(三十五):不要再买打印机小白盒了,用懒猫微服把有线打印机改成无线,还支持airprint
- URL: https://blog.no-claw.com/posts/846eeff9/
- 日期: 2026-02-14
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服, 打印机
家里的打印机有十多年了,最近使用频率不是很高,所以把打印机放在柜子里了,需要用的时候再拿出来接线——每次都像在搬砖:找线、找口、开机,试打一张、再把它塞回去。
那时候还不流行网络打印,但是现在看来确实是硬伤了。那我就计划把它改成“随时可用”。不求快,不求高级,只求你在任何设备上点一下就能打,别让我再拿着电脑跟着打印机跑。
大概前前后后摸索了这些方案:
用小白盒连接路由器:其实这个思路跟网络打印机很类似,就是打印机 over IP,企业里几乎也都是这样的做法。缺点就是需要客户端安装驱动,所以相比之下就牺牲了移动端。
windows/MacOS共享:由于缺少 airprint,所以Apple 设备无法使用隔空打印。其实 Windows 的兼容性是最好的。
在OpenWrt上安装CUPS驱动,然后打印机接路由器当做无线使用。Apple生态会舒服很多。
我在第三个方法上进行了改良:不在OpenWrt上折腾了,换成用懒猫微服充当CUPS驱动传递的Server。
我这套里懒猫微服就干三件事:跑CUPS(带AirPrint)、挂网盘做中转、把Windows远程入口映射出去。
应用商店CUPS
懒猫应用商店里直接可以下载CUPS服务端,内置了airprint协议,省去了自己折腾docker镜像的麻烦——别人测试好的我来开箱即用。
我们需要做的就是把USB直通给这个从商店下载的CUPS Docker。
这里有个很真实的小问题:每次打印机关机/重启、或者USB重新插拔以后,容器可能会读不到这个USB外设,所以每次都要重启一下容器来重新识别USB。
这个镜像内置了很多打印机驱动,所以我们选和打印机相同或者相近的型号的驱动。
我这里识别到了联想的打印机,可以直接添加:
填写信息,选择共享这个打印机。
列表里没有打印机的驱动,所以我选了兄弟的。(尽管测试下来兼容性有些问题,总之还能全平台凑合用)
打印机信息一览:
使用心得:
全平台的单页打印几乎都没什么问题,iPhone/iPad/Mac/Windows都能打,体验上就是“家里终于有一台网络打印机了”。
不过我的联想M7400 Pro在使用过程中遇到了两个很奇怪的问题,这是联想自己闭门搞驱动导致我使用的开源驱动无法适配导致的。
大图片偶尔无法打印(队列里看着像发过去了,实际就是不动)
双页打印变成四页打印
然后这一套方式也用了蛮久的。
如果你的打印机型号恰好在驱动列表里,那这套方案会非常舒服;你要做的就是商店装CUPS → 接打印机 → 选型号 → 结束。
如果型号不在列表里,那就像我这样:找个相近的驱动凑合用,能用到什么程度全凭缘分(以及厂商良心)。
转发RDP,网盘传文件打印
我是一个爱折腾的人,前面的办法只适用于局域网打印,那么广域网怎么办呢?
再纠结了好久之后,我还是弄了一台Windows,一方面是给家里人使用,一旦出问题调试难度小一些,也顺便使用了联想的官方驱动,防止他再搞幺蛾子。
首先Windows通过客户端自动挂载SMB,这样就可以直接读到懒猫网盘的文件。
我对SMB挂载顺便改了名字。
大概打印流程是这样的:
收到文件 -> 传到懒猫网盘 -> 同步windows -> RDP登录WINDOW -> 直接打印
打开SMB之后,我们就可以看到从其他终端收到的文件。微信收到的文件也可以直接分享到懒猫网盘,这一步比“RDP里传文件”舒服太多了。
懒猫微服在这期间做了两件事:
因为直接通过RDP传输文件很慢,而且移动端传起来也很麻烦。所以使用懒猫网盘来做文件的中转,私人云盘用起来就很舒服。
懒猫微服可以把windows的端口映射到公网,这样子在外边的时候也可以远程登录直接打印了,你的客户端只要有一个Window APP可以远程登录就好。
如果使用的是懒猫微服的商店的虚拟机windows,那么甚至可以使用Web VNC登录,只要想办法把打印机USB映射进去就好。
---
## Macbook Pro 虚拟机安装ARM飞牛
- URL: https://blog.no-claw.com/posts/567b12ef/
- 日期: 2026-02-14
- 分类: 电子产品 / 电脑 / NAS
- 标签: NAS
飞牛公测有一阵子了,一直没腾出时间折腾这些。手里的ARM设备有泰山派开发板和Macbook Pro。由于泰山派实在没啥资料,所以还是先用Macbook + PD 安装飞牛吧,官网也提供了安装包下载。
我本来想给泰山派也刷一个:https://www.fnnas.com/download-arm 不过我看这里没有镜像了,估计是和OEC的关系影响到了RK3566的适配。
不用UTM的原因是大家都觉得很难用,所以还是选择了Parallel Desktop,安装很丝滑,几分钟就完成了。
自动识别到了飞牛的ISO,因为FN用的debian内核,就当debian用吧。(本质就是一套 Linux 安装流程,PD 对 Debian 这套适配也成熟,少踩坑。)
然后进入图形化安装流程,其实和安装debian一样的。
选择安装磁盘,这里是系统盘。
然后无脑安装就行了,等进度条。
安装进度条结束之后就看到了FNOS的标志,没有图形GUI。会显示IP地址。(这一步其实已经装完系统了,后续基本都在 Web 后台做初始化。)
然后从IP地址进入web后台,设置管理员用户名密码。
进入系统之后我们可以看到系统盘的容量,现在没有数据盘,所以没办法安装软件。(飞牛的应用基本都要落到存储空间里:没有数据盘=没建存储池=应用中心很多东西会直接灰掉。)
而且这个IP地址是虚拟机DHCP分的,我们需要改成和局域网一个网段。(不然每次重启 DHCP 变一下 IP,就得重新找;另外后面想从别的设备访问,也希望它像“局域网里的一台 NAS”,而不是“宿主机后面的一台小黑盒”。)
先停止虚拟机,我们做配置变更,加数据盘和改WIFI配置。
停机之后,加硬盘2。
网络配置也改成和宿主机共享无线网卡。(PD 这里如果走默认的 NAT,虚拟机通常在一个私有网段里,外面设备访问会绕一圈甚至直接不通;共享无线网卡等价于让虚拟机“挂”到当前 WiFi 这张网里,局域网可达性更好。)
修改完配置重启之后,我们会看到新的IP地址,然后进入后台之后,就可以看到新的数据盘了。
创建存储空间,我这里选Btrfs。(主要图它快照/校验这些特性,NAS 场景挺合适。)
就一块盘,存储模式也无所谓了。
接下来就是格式化。
等待存储池创建完成。
然后我们就可以从应用中心安装软件了。(有存储池之后,应用的安装路径、数据目录才有地方落盘。)
配置一览。
这套在 Mac 上用 PD 跑 FNOS 的方式,优点就是“快”和“稳”:几分钟起一台 NAS 环境,装应用、建存储池、跑基础功能都够用。等啥时候有时间,再慢慢折腾泰山派安装飞牛吧。
---
## 在Macbook Pro上突破原生限制,实现菊花链三屏显示(曲线救国版)
- URL: https://blog.no-claw.com/posts/cb0bcf1b/
- 日期: 2026-02-08
- 分类: MacOS / Apple
- 标签: Apple
M芯片的Macbook Pro 原生只支持外接两个4K显示器,接第三个显示器就黑屏,意外在网上看到了display link的方案。于是买回来突破了原生的MacOS的限制。
我买的:display link 是这个配置:
输入可以切换Typec + USB
输出是俩HDMI
还有几个USB2.0: 没啥用 hhhh,也就接个耳机和键鼠
需要下载驱动Displaylink manager,然后系统里就能看到你在共享屏幕,
不管你在哪家买的硬件,应该用的都是这个软件。
Macbook Pro有三个Typec口,打算预留一个TypeC给其他设备,所以一开始用2个TypeC + 一个HDMI,而Display link也是实现的Typec转HDMI,所以整体的线就很乱。
1
2
3
4
Macbook
1. -> TypeC - 显示器1
2. -> HDMI - 显示器2
3. -> TypeC(Display link) - HDMI - 显示器3
如果用Typec普通拓展坞接出来的HDMI是4K@30, 直接TypeC就是 4k@60。
突发奇想如果直接把USB接到显示器2上呢,好消息是能亮,还能4k@60,这不就是变相的菊花链~ MacOS不支持,但是Display link可以啊!!
虽然性能不如PCLE直连,不过CPU额外的算力对M芯片也不算啥,线都扔在后面了,桌面也比以前干净了。
---
## 国民NAS 飞牛零日漏洞之后:我们需要什么样的 NAS ?
- URL: https://blog.no-claw.com/posts/75d5cfe5/
- 日期: 2026-02-06
- 分类: 懒猫微服 / 番外
- 标签: 懒猫微服, NAS
前些日子被飞牛刷屏了,因为他们的零日漏洞。对于这种事我并不意外,这种“某个厂商又被爆出高危漏洞”的新闻,在过去几年里已经见过太多次了。威联通被勒索软件批量打过,群晖也出过远程代码执行的高危洞,甚至于阿里云盘也有照片“串号”的问题。只是平时刷到这类消息的时候,大多数人都会下意识地划过去,觉得这更多是厂商的锅,恰好我们没有用这个产品,或者没有被攻击过。直到这次轮到飞牛,论坛里有人说“已经被扫到”“后台被进过”“文件目录被翻过”,甚至资料被公开售卖,我甚至想说,我只想安安静静地存个东西就那么难吗。
但是感慨的原因,并不在于某一个厂商被爆出漏洞,而在于我们一直默认家用 NAS 是一类“相对安全的设备”,但现实并不完全是这样。当我们做内网穿透、端口转发的时候,运营商会禁止我们对外提供服务,这么看来倒是一种保护了。在漫长的斗智斗勇过程中,我们也学到了不少专业知识,但如果非要用数据泄露来交学费,那实在是太过惨痛了。
先说清楚,这篇文章无意针对飞牛以及其他厂家,我也是飞牛,群晖和威联通的忠实用户。他们能在市场上有这么多用户量,靠的是真本事,这一点谁都没法否认。
但这次漏洞事件暴露出来的东西,不是换个品牌、打个补丁就能翻篇的。它让我开始琢磨一个更根本的问题:我们玩了这么多年的家用 NAS,这套远程访问的路子,是不是从架构上就有一道天花板在那摆着?
如果把情绪抽离掉,从工程视角来看,飞牛这次零日漏洞暴露出来的问题其实很清晰。攻击路径并不是“有人猜中了你的账号密码”,也不是“你没开双因素认证”。根据已公开的信息,这次事件中至少涉及一个 CVSS 评分 9.8 的严重目录遍历漏洞(CVE-2025-0510),攻击者无需任何认证即可从OS中获取敏感信息,执行任意脚本,完成从“数据窃取”到“完全控制”的整条攻击链。
从飞牛论坛用户的清理反馈来看,攻击者的持久化手段相当专业:修改系统启动脚本(system_startup.sh)植入恶意代码,加载恶意内核模块隐藏进程,给恶意文件设置 immutable 属性防止删除,甚至篡改 DNS 设置将 OTA 升级域名指向无效地址,让设备无法自动更新补丁。
有用户反馈恶意程序在凌晨三点定时激活,每隔一小时执行一次,反复清理三天才最终平息。飞牛官方虽然紧急发布了 fnOS 1.1.15 和 1.1.18 安全补丁,但对于那些已经被植入内核级后门的设备来说,补丁来得再快也已经晚了——因为攻击者早就把升级通道堵死了。
这不是飞牛独有的问题。SQL 注入是 OWASP Top 10 里最经典的漏洞类型之一,它出现在一个已经上市的 NAS 产品中,恰恰说明了一个残酷的现实:NAS 厂商的安全开发水平参差不齐,而用户对此几乎没有任何鉴别能力。放在任何一家做 NAS 的厂商身上,本质都是同一个模型下的必然风险。
我跟很多人的第一反应一样:后台开了 MFA,SSH 加了密钥认证,密码也够复杂,应该没事吧?
但这次事件让我不得不面对一个很残酷的事实:这些防护手段,防的是“有人试图登录你的账号”,而不是“有人绕过登录直接打穿你的服务”。零日漏洞的可怕之处恰恰在于,它往往发生在认证流程之外。攻击者不需要知道你的密码,不需要通过你的 MFA,他直接请求一个有漏洞的接口,就能拿到系统权限。你精心配置的那些安全措施,在这条攻击路径上根本没有出场的机会。
就像你给大门装了三道锁,但小偷是从窗户翻进来的。
这件事真正让我警醒的,是它逼我重新审视“家用 NAS 到底是什么”这个问题。我们习惯把 NAS 当成一个“带点智能的网盘”,买回来插上硬盘,装几个套件,配好远程访问,就觉得万事大吉了。
但从技术结构上看,它更像是一台长期在线的小服务器:有 Web 管理后台,有文件服务接口,有各种插件和后台服务在跑,有远程访问需求,有公网可达入口。换句话说,它具备了服务器的全部攻击面,却经常被用户用消费电子的心态来对待。我们会很自然地为了方便打开端口映射、配置 DDNS,让设备可以随时在外网访问,却很少有人真正意识到:这相当于把一台服务复杂、补丁节奏完全依赖厂商的服务器,直接放在了公网边缘,24 小时不间断地接受全球扫描器的“体检”。
飞牛的零日漏洞,其实只是把这个结构性问题放大到了所有人眼前。
我后来花了不少时间去想一个问题:为什么 NAS 的安全事故总是以这种方式发生?不是用户密码太弱,不是配置太离谱,而是厂商的某个服务组件出了洞,然后大量暴露在公网的设备被批量扫描、批量利用。
想来想去,答案其实很简单——因为传统 NAS 的远程访问模型,从根子上就是“让公网能直接打到你家设备”。无论你是通过端口映射、DDNS,还是厂商提供的云中转服务,最终都意味着你的管理面或服务面在公网有一个可被发现的入口。这个模型在便利性上没问题,但安全性完全依赖两个前提:厂商永远不出漏洞,以及用户永远配置正确。而这两个前提,在现实中从来没有同时成立过。
做过安全的人都知道一个很朴素的原则:缩小攻击面,永远优先于修补漏洞。与其指望“厂商永不出洞”,不如从架构上减少公网暴露。如果攻击者连你的设备都扫不到,那么即使系统里存在未知漏洞,被利用的概率也会断崖式下降。这不是什么高深的安全理论,这是安全工程的第一课。但遗憾的是,绝大多数消费级 NAS 的产品设计,并没有把这个原则放在优先位置。它们更关心的是“用户能不能方便地远程访问”,而不是“这种访问方式是否在架构上足够安全”。
也正因为这样,我后来开始反思一个更根本的问题:是不是我们一开始就选错了“家用私有云”的技术路线?传统 NAS 的远程访问模型,本质上是假设“公网直连 + 用户自行加固”,这在早期小规模使用时尚且可控,但随着 NAS 功能越来越复杂、用户群体越来越非技术化,这种模型注定会不断放大安全风险。它把服务器级别的安全责任,转嫁给了普通家庭用户,而这本身就是一个不太合理的设计前提。你不能一边把产品卖给“想要一个家庭网盘”的普通人,一边要求他们具备运维一台公网服务器的安全能力。
想清楚这一点之后,我重新选方案的第一标准就变了:不是功能多不多,不是品牌响不响,而是——公网能不能扫到我。在这个标准下,任何能做到“默认不暴露公网”的方案,都比传统 NAS 架构更符合我的安全预期。
后来接触到懒猫微服的时候,说实话我一开始是带着怀疑态度的。市面上打着“私有云”旗号的产品太多了,很多不过是换了个壳的 NAS。但真正让我停下来认真看的,是它的整体架构设计思路。
根据懒猫微服官方开发者文档的描述,懒猫微服的系统分为三层架构:最底层是一个极度精简的底层系统,只负责网络连接、安全认证,以及业务操作系统的启动和更新,目标是保证不管怎么升级,系统永远不会挂;中间层是业务操作系统,负责网络隔离、应用调度、资源管理;最上层是 LPK 应用层,这是懒猫自己的容器格式,官方强调这种容器格式在权限隔离与运行时控制上的安全边界更可控,并计划逐步补充网络流量审计、网络限制和用户权限控制等能力,用来尽量降低应用层对系统整体安全的影响。这种分层设计的好处很直观:底层保证稳定性,中间层保证隔离性,应用层尽量把风险限制在可控范围内,不会因为某一层出问题就牵连整个系统。
但真正让我觉得这个产品在安全思路上跟传统 NAS 拉开差距的,是它的网络传输机制。懒猫微服的远程访问分为两种模式:当终端设备所在网络具备 IPv6 或 NAT3 条件时,系统会自动与用户设备建立直连传输;当网络环境较差时,系统会自动切换到中继数据传输服务。关键在于,无论哪种模式,数据传输都采用端到端加密技术,传输内容对包括平台运营团队在内的任何第三方均不可见。而且这个穿透服务属于系统网络层能力,所有应用——包括官方应用、开发者自主开发的应用、甚至开发者搭建的虚拟机——都自动受益,不需要用户为每个服务单独配置网络穿透规则。
这意味着什么?意味着你不需要在路由器上开端口映射,不需要配置 DDNS,不需要自己搭建穿透机制或反向代理。设备是主动向外建立加密通道的,公网扫描器看到你家的公网 IP,也发现不了任何可直接访问的服务入口。这从根本上改变了攻击面模型:传统 NAS 是“我把服务摆在公网等你来连”,懒猫微服更像是“我主动出去建立连接,但外面的人看不到我”。对于这类零日漏洞的攻击场景——攻击者批量扫描公网 IP、发现暴露的管理接口、利用漏洞打进去——在这种架构下,第一步就很难成立,因为根本没有暴露在公网的入口可以被扫描到。
当然,这种“默认不暴露公网、依赖穿透与中继”的模型也不是没有代价:它对平台控制平面的稳定性依赖更强,也意味着用户在一定程度上需要信任厂商的网络基础设施能力。安全复杂度从用户侧转移到了平台侧,这对厂商自身的安全工程能力提出了更高要求。一旦平台侧出现故障,设备本身是安全的,但远程访问体验可能会受到影响,这是这种模型天然需要承担的代价。而且作为一个相对年轻的产品,它的生态成熟度跟老牌 NAS 厂商相比还有差距,自研的容器格式也需要更长时间的社区检验。这些都是选择这种架构路线之前必须接受的现实成本。
懒猫微服的理念文档里有一段话让我印象很深。它的创始人做了二十多年开源社区,他观察到的一个长期问题是:当个人数据逐步被公有云化之后,商业模式往往会不可避免地走向基于数据的变现逻辑。懒猫微服希望通过私有硬件把个人数据重新放回用户手里。这句话本身听起来像口号,但如果从产品架构反推,这确实是它很多设计选择背后的出发点——例如默认不暴露公网入口、强调端到端加密、尽量减少对第三方的信任假设。当一个产品从理念层面就把“数据尽量掌握在用户自己手里”当成优先目标,它在安全设计上的取舍,确实会和那些更偏向功能堆叠的产品不太一样。
从工程实现的角度看,懒猫微服更像是在家庭场景下尝试零信任网络的落地:默认不信任公网,默认不暴露服务,通过身份认证与加密通道来建立访问路径。它并不能保证系统永远没有漏洞——没有任何系统能做到这一点——但它把风险从“公网随意可达、被扫到就可能被打穿”,压缩到了“必须先突破身份体系和加密隧道”的范围内,攻击成本的量级明显不在一个级别上。
硬件层面的事情也值得单独提一下,因为很多人忽略了一个事实:安全能力本身是吃资源的。端到端加密、容器隔离、服务沙箱、AI 本地索引,这些能力全部打开之后,对 CPU 和内存的消耗是实打实存在的。很多入门级 NAS 为了控制成本,硬件资源本身就偏紧张,用户为了“用得顺”,最后只能关掉一部分功能,甚至包括一些安全相关的能力。懒猫的硬件定位更偏向“家庭私有云工作站”,整体资源相对充裕一些,所以在实践中更容易做到“该开的安全能力不需要为了性能被迫关闭”。这一点对安全体验的影响,其实比参数表本身更重要。
还有一点我觉得值得单独说的,是这个团队给人的感觉。
用过 NAS 的人大概都有过类似体验:遇到问题去官方论坛发帖,要么石沉大海,要么收到比较模板化的回复,很难确认对面是否真正理解你的具体场景。
懒猫这边给我的感受完全不一样。他们的一线技术支持是真正懂 Linux 的工程师在做,不是外包给念话术脚本的客服。你提一个问题,回你的人可能就是写那段代码的人,给出的不是套话,而是具体的排查思路或实现路径。用他们自己的话说,逻辑其实很简单:我们是最会玩 Linux 的那批人,你要的我们都有,花钱支持我们的客户,我们就实时响应。这种态度在国内硬件厂商里不多见,对我这种爱折腾的人来说,这种沟通方式本身就是产品体验的一部分。
实际使用下来也确实如此。懒猫有自己的用户论坛,也有比较活跃的社群,用户反馈的问题经常能看到开发团队跟进。社区里有一个长期存在的主题是“说出你的需求,我们来移植”,开发者会协助把用户常用的自托管应用移植到他们的应用体系里。官方还提供了一些社区激励机制,鼓励开发者参与生态建设。这些机制本身并不决定产品好坏,但它们让生态的扩展变成了一种可持续的系统行为,而不是完全依赖官方单点投入。
我在论坛上写了 80 多篇懒猫微服的实战连载,从开箱到进阶到炫技,写着写着就停不下来了。推荐你在论坛的攻略里感受到我折腾这台机器的状态:《懒猫智慧屏,我以为是地表最强电视盒子,结果竟然可以改装成闺蜜机?》《用懒猫微服倒推停电时间》《sunshine+moonlight 双人串流打游戏》《蓝牙音浪,懒猫开唱》《西湖邂逅后,我手把手教她玩转 NAS》《坏掉的 Windows 不要扔,硬盘插在懒猫上还能用》《服务器宕机之后,我和前端靠懒猫微服结对编程》。这些都是我在真实场景里折腾出来的记录。写着写着发现社区里其他人也在这么玩:有人用它存 Steam 游戏,有人用它替代 1Password 订阅,有人用它给大疆 Pocket3 导素材,有人用它做 24×7 在线开发机,有人用它的穿透服务让车机远程听黑群晖的歌。这些用法本身说明的不是“产品多强”,而是这种架构在家庭场景下确实覆盖了一些真实需求。如果你恰好是开发者,参与应用移植的过程本身也是学习——Docker 实践、npm 构建这些东西,社区里的工程师会直接带着你走一遍,至少在某个深夜,帮我解答了 docker 的 bind mount 和 volume 的异同和场景对比。
说回我自己现在的方案。折腾了这么一圈之后,我最终把家里的存储和服务迁到了懒猫微服上。不是因为它完美,而是因为它的安全模型让我第一次觉得“在远程访问这件事上不用操心”。不用开端口,不用配 DDNS,不用自己搭反向代理,也不用担心哪天又爆出一个新的零日漏洞就得手忙脚乱地去堵洞——因为公网本身看不到我的设备。端到端加密是默认开启的,平台侧也无法看到你的数据内容。三层系统架构把底层稳定性、业务隔离和应用安全分得比较清楚,不至于因为装了一个有问题的应用就牵连整个系统。
对我个人而言,权衡之后答案很明确:我宁可接受“平台偶尔不稳定”带来的体验波动,也不太愿意继续承担“设备 24 小时暴露在公网”这种结构性风险。前者更多是体验问题,后者本质上是安全问题,两者的风险量级并不在一个维度上。
如果一定要给这次零日漏洞事件一个更有价值的意义,那可能不是提醒我们“哪个厂商不安全”,而是提醒我们:家用 NAS 这条路,本身就存在一个被长期忽视的安全天花板。这个天花板不是靠换品牌能突破的,也不是靠多加几层认证能突破的,它是由“公网暴露”这个基本架构决定的。
这大概是这次事件教给我的最重要一课:安全这件事,靠补洞永远是被动的,靠架构才是主动的。选 NAS 不是在选品牌,而是在选默认风险模型。
---
## 使用 AWS SES + S3 发送 HTML 邮件
- URL: https://blog.no-claw.com/posts/d51f292a/
- 日期: 2026-02-01
- 分类: 软件 / AWS
- 标签: AWS
在营销、通知等场景中,我们经常需要发送格式丰富的 HTML 邮件。本文介绍如何用 Python + boto3,从 S3 读取 HTML 模板并通过 SES 发送邮件。
架构
S3 (HTML模板) → Python脚本 → SES → 收件人
前置条件
AWS 账号已开通 SES 服务,且发件地址已验证
S3 Bucket 中已上传 HTML 模板文件
本地已配置 AWS 凭证(aws configure 或 IAM Role)
安装依赖:pip install boto3
核心代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/env python3
"""从 S3 读取 HTML 模板并通过 SES 发送邮件"""
import boto3
REGION = ''
BUCKET = ''
TEMPLATE_KEY = '.html'
s3 = boto3.client('s3', region_name=REGION)
ses = boto3.client('ses', region_name=REGION)
def get_html_from_s3(bucket: str, key: str) -> str:
"""从 S3 读取 HTML 内容"""
response = s3.get_object(Bucket=bucket, Key=key)
return response['Body'].read().decode('utf-8')
def send_html_email(to: str, subject: str, html_content: str):
"""发送 HTML 邮件"""
ses.send_email(
Source='',
Destination={'ToAddresses': [to]},
Message={
'Subject': {'Data': subject, 'Charset': 'UTF-8'},
'Body': {'Html': {'Data': html_content, 'Charset': 'UTF-8'}}
}
)
print(f'邮件已发送至 {to}')
if __name__ == '__main__':
html = get_html_from_s3(BUCKET, TEMPLATE_KEY)
send_html_email('', '测试邮件', html)
关键点说明
get_html_from_s3:通过 s3.get_object 拉取 HTML 文件内容,注意 decode(‘utf-8’) 确保中文正常显示
send_html_email:调用 ses.send_email,将 HTML 作为邮件 Body 发送,指定 Charset: UTF-8 避免乱码
Source 地址必须是 SES 中已验证的邮箱或域名
---
## 一次拿trace把langfuse打挂的修复
- URL: https://blog.no-claw.com/posts/c4eda619/
- 日期: 2026-01-31
- 分类: 软件
- 标签: LLM, Langfuse
用 Langfuse 做 LLM 观测平台,拉 trace 数据时不小心把服务端打挂了。本文记录从发现 502 到定位 Node.js OOM,再到写脚本安全导出标注数据的完整过程。
Langfuse 是一个开源的 LLM 观测平台,用来追踪 LLM 应用的调用链路、记录 input/output、做人工标注评估等.
跑了一段时间,积累了不少 trace 数据和人工标注。某天想通过 API 批量拉取 trace 数据做分析,结果把服务端打挂了。
故障现象
第一阶段:502 后端超时
请求 /api/public/traces/{id} 接口拉取单个 trace 的完整数据时,先是返回 502 Bad Gateway,Nginx/OpenResty 报后端超时。
第二阶段:整个应用挂了
多请求几次之后,不只是 API 超时了,整个 Langfuse Web 界面都打不开了,彻底 503。
根因分析
把容器日志扔给 AI 分析,定位到了问题:
因果链
1
2
3
4
5
大 trace 请求
→ 服务端序列化 >4MB 的响应体
→ Node.js 堆内存爆了(OOM)
→ 进程崩溃
→ Nginx/OpenResty 返回 502/503
具体原因
Langfuse 是 Next.js 应用,跑在 Node.js 上,默认堆内存上限大约 2GB
我请求的那几个 trace 数据量很大,每个响应体超过 4MB(日志里反复提示 exceeds 4MB)
多个大 trace 请求同时处理时,Node.js 内存直接爆了,进程崩溃
进程挂了之后,前面的反向代理(Nginx/OpenResty)拿不到后端响应,就返回 502/503
核心问题
/api/public/traces/{id} 这个接口会返回 trace 的完整数据,包括所有 observations、spans、events 的全部 input/output。如果一个 trace 里有多轮 LLM 调用,每轮的 prompt 和 completion 都很长,那整个 trace 的 JSON 响应轻松超过 4MB。
Node.js 在序列化这么大的 JSON 时,内存占用会远超 JSON 本身的大小(因为要构建字符串、做 UTF-8 编码等),几个大 trace 同时处理就足以把 2GB 堆内存撑爆。
解决思路
我的需求其实很简单:导出所有被人工标注为 “Good” 的 trace 的 input/output,用来做后续的微调数据集。
既然直接拉 trace 会把服务端打挂,那就绕开它:
接口
返回内容
风险
/api/public/traces/{id}
完整 trace(所有 spans、events、input/output)
响应体巨大,容易 OOM
/api/public/observations
按 traceId 查询 observations
数据量可控,安全
/api/public/scores
所有标注数据(不含 trace 内容)
很轻量
策略:
先通过 /api/public/scores 拿到所有标注,过滤出 Good 的
再通过 /api/public/observations?traceId=xxx 逐个拉取对应的 input/output
加上重试和限流,避免再次打挂服务端
抢救脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import requests
import json
import time
PUBLIC_KEY = "pk-lf-xxx"
SECRET_KEY = "sk-lf-xxx"
BASE_URL = "https://your-langfuse-instance.example.com"
session = requests.Session()
session.auth = (PUBLIC_KEY, SECRET_KEY)
def get_with_retry(url, params=None, max_retries=3):
"""带重试的 GET 请求"""
r = None
for i in range(max_retries):
try:
r = session.get(url, params=params, timeout=60)
if r.status_code == 200:
return r
print(f" 第{i+1}次请求返回 {r.status_code},等待重试...")
except Exception as e:
print(f" 第{i+1}次请求异常: {e}")
time.sleep(3)
return r
# 第一步:获取所有人工标注
t0 = time.time()
print(f"[{time.strftime('%H:%M:%S')}] === 获取标注数据 ===")
resp = get_with_retry(
f"{BASE_URL}/api/public/scores",
params={"source": "ANNOTATION"}
)
print(f"[{time.strftime('%H:%M:%S')}] 获取标注耗时 {time.time()-t0:.2f}s, status={resp.status_code}")
if not resp or resp.status_code != 200:
print(f"请求失败: {resp.status_code if resp else 'None'}")
exit()
scores = resp.json().get("data", [])
print(f"共找到 {len(scores)} 条标注")
# 第二步:过滤 Good 的
good_scores = [
s for s in scores
if s.get("value") == 1 or s.get("stringValue") == "Good"
]
print(f"其中 Good 的有 {len(good_scores)} 条")
# 第三步:用 observations API 逐个拉取(比 traces 轻量,不会撑爆服务端)
def fetch_observations(trace_id):
"""拉取 trace 下的 observations,只取 GENERATION 类型的 input/output"""
try:
r = get_with_retry(
f"{BASE_URL}/api/public/observations",
params={"traceId": trace_id}
)
if not r or r.status_code != 200:
return None
obs_list = r.json().get("data", [])
# 取 GENERATION 类型的(即 LLM 调用的 input/output)
generations = [o for o in obs_list if o.get("type") == "GENERATION"]
if generations:
# 取最后一个 generation(通常是最终输出)
gen = generations[-1]
return {"input": gen.get("input"), "output": gen.get("output")}
# 没有 generation 就取第一个 observation
if obs_list:
o = obs_list[0]
return {"input": o.get("input"), "output": o.get("output")}
return None
except Exception as e:
print(f" 获取 observations 失败: {e}")
return None
results = []
t1 = time.time()
print(f"[{time.strftime('%H:%M:%S')}] 开始逐个拉取 observations...")
for i, s in enumerate(good_scores):
trace_id = s.get("traceId")
t = time.time()
obs = fetch_observations(trace_id)
elapsed = time.time() - t
record = {
"trace_id": trace_id,
"score_name": s.get("name"),
"score_value": s.get("value"),
"string_value": s.get("stringValue"),
"comment": s.get("comment"),
"input": obs.get("input") if obs else None,
"output": obs.get("output") if obs else None,
}
results.append(record)
print(f"[{time.strftime('%H:%M:%S')}] {i+1}/{len(good_scores)} trace={trace_id[:8]}... {elapsed:.2f}s")
time.sleep(0.5) # 限流,别再把服务打挂了
# 第四步:保存
print(f"[{time.strftime('%H:%M:%S')}] 拉取总耗时 {time.time()-t1:.2f}s")
with open("good_annotations.json", "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2, default=str)
print(f"[{time.strftime('%H:%M:%S')}] 全部完成,总耗时 {time.time()-t0:.2f}s")
print(f"已导出 {len(results)} 条 Good 标注到 good_annotations.json")
脚本设计要点
绕开 traces 接口:不用 /api/public/traces/{id},改用 /api/public/observations?traceId=xxx,返回的数据量小得多
重试机制:get_with_retry 最多重试 3 次,每次间隔 3 秒,应对偶发的超时或 5xx
限流:每个请求之间 sleep(0.5),避免并发请求再次打挂服务端
只取需要的数据:从 observations 里只取 GENERATION 类型的 input/output,不拉完整的 span 树
输出格式
导出的 good_annotations.json 长这样:
1
2
3
4
5
6
7
8
9
10
11
[
{
"trace_id": "abc12345-...",
"score_name": "quality",
"score_value": 1,
"string_value": "Good",
"comment": "回答准确",
"input": { "messages": [{ "role": "user", "content": "..." }] },
"output": { "choices": [{ "message": { "content": "..." } }] }
}
]
每条记录包含 trace ID、标注信息、以及对应的 LLM input/output,可以直接用来构建微调数据集。
经验总结
关于 Langfuse
Langfuse 的 /api/public/traces/{id} 接口会返回完整的 trace 数据,如果 trace 里有大量 LLM 调用,响应体很容易超过 4MB
Node.js 序列化大 JSON 时内存占用远超 JSON 本身大小,几个大请求就能把默认 2GB 堆内存撑爆
批量拉数据时,优先用 /api/public/observations 和 /api/public/scores 这类更轻量的接口,按需取数据
如果确实需要拉大 trace,考虑加 --max-old-space-size 参数给 Node.js 扩大堆内存,或者在 Langfuse 前面加请求大小限制
关于数据抢救
服务挂了不要慌,数据还在数据库里,只是 Web 服务进程崩了
重启容器通常就能恢复,但要避免再次触发同样的问题
写抢救脚本时,重试 + 限流 + 只取必要字段,三件套缺一不可
自建 Langfuse 的好处是数据完全在自己手里,但也意味着运维问题得自己扛
相关链接:
Langfuse 官网:https://langfuse.com
Langfuse API 文档:https://api.reference.langfuse.com
---
## 懒猫微服进阶心得(十五):超越systemd,不用每次开机都再安装软件~
- URL: https://blog.no-claw.com/posts/c1ba52cc/
- 日期: 2026-01-30
- 分类: 懒猫微服 / 进阶
- 标签: Docker, 懒猫微服
前言
懒猫微服是分层文件系统,所以在之前的文章里前面我们使用用 systemctl --user 在开机时自动安装软件,解决重启丢包的问题。但说实话,每次开机都要跑一遍 apt install,当面对软件包过多以及网络延迟的问题的时候,使用dkpg会卡住,这不够优雅,所以才有了这个方案。
后来我换了个思路:既然懒猫微服天生就是为 Docker 而生的,为什么不直接用容器来跑这些工具呢?
想想看,htop、iotop、glances 这些运维神器,本质上就是读取 /proc 和 /sys 里的系统信息。只要让容器能访问宿主机的这些文件,不就能正常工作了吗?
这篇文章就来聊聊怎么用 Docker 容器跑系统监控工具。不用装软件,不怕重启丢失,一条命令就能用,用完自动清理。这让我们既享受了分层文件系统的优势,也不用再为软件持久化烦恼了。
关于镜像源:本文使用了 docker.1ms.run 作为镜像加速站,仅作示例,不对镜像源的可用性负责。如果某个镜像拉取失败,可以换成其他镜像源,比如 dockerpull.org、docker.xuanyuan.me 等。
进程监控类
htop - 经典进程查看器
我们习惯的使用方式是 apt install htop,但在懒猫微服的分层文件系统下重启后会丢失。所以我们可以用Docker来一键设置:
1
pg-docker run --rm -it --pid host docker.1ms.run/frapsoft/htop
这条命令的作用是:启动一个包含 htop 工具的 Docker 容器,并以交互式方式查看宿主机的进程列表。容器停止后会自动删除。htop 是一个用于实时查看和管理系统进程的交互式工具。我们可以逐部分解析一下这条命令:
docker run:
这是 Docker 命令,用来启动一个新的容器并执行指定的命令。
--rm:
这个选项表示容器在停止后会被自动删除。这样可以避免容器停留在系统上,占用空间。
-it:
-i 表示让容器保持交互模式(interactive),即允许你输入命令。
-t 表示分配一个伪终端(tty),让你可以以终端方式与容器交互。
--pid host:
这个选项使得容器能够访问宿主机的进程信息。正常情况下,容器只能看到自己内部的进程,但通过 --pid host,容器可以查看宿主机的所有进程,这样你就可以通过 htop 查看宿主机的进程。
frapsoft/htop:
这是一个 Docker 镜像的名称,它提供了一个包含 htop 工具的容器镜像。运行这个镜像会启动 htop,让你可以在容器中查看进程。
为了方便,还可以在 .bashrc 里加个 alias,这样就能像本地命令一样直接敲 htop 使用了。
1
2
echo "alias htop='pg-docker run --rm -it --pid host docker.1ms.run/frapsoft/htop'" >> ~/.bashrc
source ~/.bashrc
在重启之后我们看到镜像没有丢失,还在本地存在,所以这个方式是可行的。
1
2
3
4
lzcbox-e66ccc4a ~ # pg-docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
docker.1ms.run/library/alpine latest a40c03cbb81c 3 days ago 8.44MB
docker.1ms.run/frapsoft/htop latest 4b76ab24430c 9 years ago 8.19MB
如果你经常玩docker,那么除了–pid之外一定都不陌生。对于默认来讲,–pid 的参数是 private,所以我们常说每个容器有独立的进程命名空间,容器只能看到自己内部的进程。用了**--pid host** 之后,容器就可以共享宿主机的进程命名空间。这样,容器可以查看宿主机上的所有进程,而不仅仅是容器内部的进程。所以我们可以用它来做一些监控。甚至还有,**--pid container:**:- 容器将能够看到并与指定容器的进程共享命名空间。通常在多容器间需要共享进程信息或调试时使用。例如,一个容器需要查看另一个容器的进程或进行故障排查。当然我们这里不做仔细展开。
关于 Docker 参数解释以及 alias、bashrc 的修改,这里只以 htop 为例,后面的工具不再赘述。
同理我们也可以测试其他工具。
glances - 全能系统监控
1
2
3
pg-docker run --rm -it --pid host --net host \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
docker.1ms.run/nicolargo/glances
glances 比 htop 更全面,是个「一屏看所有」的监控工具。CPU、内存、网络、磁盘、Docker 容器状态全都有。参数说明:
--pid host:读取宿主机进程信息
--net host:读取宿主机网络信息
-v docker.sock:让 glances 能监控 Docker 容器
btop - htop 的现代化替代
1
pg-docker run --rm -it --pid host terorie/btop
btop 界面更炫酷,功能也更丰富,支持鼠标操作。不过这个镜像比较冷门,docker.1ms.run 等加速站可能没缓存,所以这里直接用 Docker Hub 官方源。如果拉取慢,可以换成其他加速源试试。
ctop - Docker 容器专用监控
1
2
3
pg-docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
quay.io/vektorlab/ctop
这个镜像来自 quay.io(Red Hat 的镜像仓库),国内访问通常比 Docker Hub 顺畅,可以直接拉取。ctop 专门用来监控 Docker 容器的资源占用。
不过要注意,ctop 默认连接 /var/run/docker.sock,这是系统 Docker 的 socket。如果你想监控 playground 里的容器,需要挂载 playground 的 socket:
1
2
3
pg-docker run --rm -it \
-v /data/playground/docker.sock:/var/run/docker.sock:ro \
quay.io/vektorlab/ctop
netshoot - 网络排障瑞士军刀
1
pg-docker run --rm -it --net host --pid host --privileged docker.1ms.run/nicolaka/netshoot
netshoot 内置了 tcpdump、iftop、nmap、curl、dig 等一堆网络工具,临时排查问题特别方便。网络监控工具必须加 --net host,否则测的是容器网络而不是宿主机网络。
进入容器后,先用 ip link 查看网卡名。懒猫微服的主网卡通常是 enp2s0 而不是 eth0:
1
2
3
4
# 进入容器后
ip link # 查看网卡列表
iftop -i enp2s0 # 监控主网卡流量
tcpdump -i enp2s0 # 抓包
注意:netshoot 里没有 nethogs,如果需要按进程查看流量,可以用 alpine 临时安装:
1
pg-docker run --rm -it --net host --pid host --privileged alpine sh -c "apk add --no-cache nethogs && nethogs"
工具选择指南
工具
特点
适用场景
htop
轻量、经典
快速查看进程,日常使用
btop
界面炫酷、支持鼠标
喜欢好看界面的用户
glances
功能全面、一屏显示所有
需要综合监控的场景
ctop
专注 Docker 容器
监控容器资源占用
netshoot
网络工具箱
网络排障、抓包分析
理解 –pid host 的本质
--pid host 的本质是让容器挂载宿主机的 /proc,而不是容器自己隔离的那份。
容器内的监控工具之所以能工作,是因为 Linux 的系统信息都存在 /proc 虚拟文件系统里:
文件
内容
/proc/cpuinfo
CPU 信息
/proc/meminfo
内存信息
/proc/uptime
运行时间
/proc/version
内核版本
/proc/loadavg
系统负载
/proc//stat
进程状态
常见工具的数据来源:
工具
读取的文件
htop/top
/proc//stat, /proc/meminfo, /proc/loadavg
free
/proc/meminfo
df
/proc/mounts, /proc/diskstats
ps
/proc//status, /proc//cmdline
netstat/ss
/proc/net/tcp, /proc/net/udp
vmstat
/proc/stat, /proc/vmstat
iostat
/proc/diskstats
所以 --pid host 能让这些工具在容器里正常工作,因为它们本质上就是读文件,共享了 /proc 就等于共享了系统信息。
/proc vs /etc/os-release
有个细节需要注意:/proc/version 和 /etc/os-release 记录的是不同层面的信息。
/proc/version 是内核信息,由内核动态生成:
1
2
3
cat /proc/version
Linux version 6.16.6-x64v3-xanmod1 (root@runner-6areqvs9r-project-51590166-concurrent-0) (Debian clang version 19.1.7 (3), Debian LLD 19.1.7) #0~20250909.g439cb47 SMP PREEMPT_DYNAMIC Tue Sep 9 22:24:21 UTC
/etc/os-release 是发行版信息,是静态文件:
1
2
3
4
5
6
7
8
9
10
11
cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
打个比方:
/proc/version = 引擎型号(Linux 6.16)
/etc/os-release = 车的品牌(Debian Linux)
同一个内核可以跑不同发行版,所以如果你想让容器里的工具显示正确的操作系统名称,需要手动挂载 /etc/os-release。
进阶:自己打包docker工具
我们经常习惯使用,fastfetch 显示系统信息,但还是没有找到别人打包好的容器的版本。fastfetch 是 neofetch 的现代替代品,速度更快。
我们可以用使用alpine安装二进制包的方式来运行,也可以自定义Docker images来跑:
先看一个使用alpine安装的:
1
2
3
4
5
6
pg-docker run --rm -it \
--pid host \
--net host \
-v /etc/os-release:/etc/os-release:ro \
-v /etc/hostname:/etc/hostname:ro \
docker.1ms.run/library/alpine sh -c "apk add --no-cache fastfetch && fastfetch"
参数说明:
--pid host:让 fastfetch 读到宿主机的 CPU、内存、内核信息
-v /etc/os-release:/etc/os-release:ro:让 fastfetch 读到正确的操作系统名称
-v /etc/hostname:/etc/hostname:ro:让 fastfetch 读到正确的主机名
虽然不是很完美,但是已经可以读取到大多数的信息了,
再看一个自己打包的Docker版本。
1
2
3
FROM alpine:latest
RUN apk add --no-cache fastfetch
CMD ["fastfetch"]
构建并运行:
1
2
3
4
pg-docker build -t my-fastfetch .
pg-docker run --rm -it --pid host \
-v /etc/os-release:/etc/os-release:ro \
my-fastfetch
同理我们也可以基于alpine构建基于任何软件的镜像工具,我这里再举一个例子,比如iotop。
1
2
pg-docker run --rm -it --pid host --privileged docker.1ms.run/library/alpine sh -c "apk add --no-cache iotop && iotop -o"
关于 /sys 文件系统
除了 /proc,Linux 还有 /sys 虚拟文件系统,存放硬件相关信息:
1
2
3
4
5
6
7
8
9
10
11
# 读网卡 MAC 地址
pg-docker run --rm -it -v /sys:/sys:ro docker.1ms.run/library/alpine cat /sys/class/net/enp2s0/address
# 列出所有网卡
pg-docker run --rm -it -v /sys:/sys:ro docker.1ms.run/library/alpine ls /sys/class/net/
# 读 CPU 温度
pg-docker run --rm -it -v /sys:/sys:ro docker.1ms.run/library/alpine cat /sys/class/thermal/thermal_zone0/temp
# 读磁盘大小
pg-docker run --rm -it -v /sys:/sys:ro docker.1ms.run/library/alpine cat /sys/block/nvme0n1/size
其他命名空间参数
除了 --pid host,Docker 还支持其他命名空间共享:
1
2
3
4
5
6
7
8
9
10
11
# 共享网络命名空间
--net host
# 共享 UTS 命名空间(主机名和域名)
--uts host
# 共享 IPC 命名空间(进程间通信)
--ipc host
# 共享指定容器的进程命名空间
--pid container:
--pid container: 在多容器调试时很有用,可以让一个容器看到另一个容器的进程。
关于 –privileged
你可能注意到有些命令用了 --privileged,这个参数会让容器获得几乎所有的宿主机权限:
访问所有设备(/dev/*)
加载内核模块
修改系统配置
访问完整的 /proc 和 /sys
像 iotop、nethogs 这类需要深度访问内核信息的工具,没有 --privileged 可能跑不起来。但要注意,这个参数权限很大,只在信任的镜像上使用,用完记得 --rm 自动清理。
总结
懒猫微服的分层文件系统固然好,但是常用软件会有丢失安装的问题,所以我们用容器来跑这些监控工具,容器镜像重启不会丢失,配置一个alias和bashrc就可以完美兼容这个feature。
容器里读取宿主机的参数有这三板斧:
看进程用 --pid host
看网络用 --net host
看磁盘用 --privileged
记住这三个,大多数监控工具都能在容器里跑起来。
回头看看,之前用 systemctl --user 开机自动装软件,虽然能用,但每次重启都要跑一遍 apt install,还会受限于软件体积和网络问题。现在换成 Docker 镜像,拉一次就永久缓存,重启秒开,彻底告别「开机等安装」的烦恼。
超越systemd,不用每次开机都再安装软件~
---
## 懒猫微服实战入门(三十四):方寸之间,自有天地:懒猫微服,男人的网络瑞士军刀
- URL: https://blog.no-claw.com/posts/5226a270/
- 日期: 2026-01-30
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服, 网络
曾几何时,中文互联网圈流行着这样一个段子,叫做中年男人三件套:NAS、软路由、充电头。
我们今天就来聊聊第一个。NAS 似乎是给职业运维人员的福音,而广大的爱好者们通常都是野路子,靠着一腔孤勇或者是兴趣来维护自己的小小世界,能够借鉴参考的,也就是互联网的帖子以及各种群里的答疑而已。
靠着坚持不懈,入门了 Linux 和网络,但是不求甚解,安静的文件存储,只躺在方寸之间。
如果你恰好有一台懒猫微服,那么我们正好可以一起来学习这繁杂的网络,拆解这美丽的网络新世界。
我从事过几年的云行业,在发烧友和职业人员之间横跳,于是心有所感,立志让爱好者可以有专业的技术,让职业人员可以真的产生兴趣。这是网络篇。
首先你一定听说过 IP 地址,这是互联网通信的门牌号。我们的手机、电脑,包括懒猫微服都有一个 IP 地址。当你连上网络的时候,这个地址就被分配给了设备。准确地说,是分配给了网卡,所以懒猫微服可以用转接口来拓展第二张网卡。
连上网线之后,第一步我们习惯在路由器上看 IP 地址,然后 ping 一下确认连通。懒猫微服使用 IPv6,可以用 ping -6 或 ping6。
循迹(ping、telnet)
1
2
3
4
5
ping 192.168.x.x
# 如果是 IPv6
ping -6 xxx.heiyu.space
# 或者
ping6 xxx.heiyu.space
我们也可以用 dig 查看域名解析的 IP 地址:
1
2
3
4
dig +short xxx.heiyu.space # 因为是 IPv6,没有 IPv4 所以解析不到
dig AAAA +short xxx.heiyu.space # 查 IPv6
dig @8.8.8.8 xxx.heiyu.space AAAA +short # 指定 DNS 服务器查询,也是可以查询到的
nslookup xxx.heiyu.space # Windows 可以使用这个
在登录之前,可以用 telnet 看端口连通性,然后 SSH 上去。不过一般来说,直接 ssh root@域名/地址 就可以登录了。
1
telnet xxx.heiyu.space 22
登录之后,我们可以使用 ifconfig 来看网卡信息。主要有这几张网卡比较有用:
enp2s0 物理网卡,连接家里路由器
wlp129s0 无线网卡
lo 回环接口,本机访问本机用(127.0.0.1)
heiyu-0 懒猫穿透隧道,IPv6 内网穿透用
docker0 Playground Docker 默认网桥
或者使用 ip addr,会更加现代一点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
lo: mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host noprefixroute
valid_lft forever preferred_lft forever
2: enp2s0: mtu 1500 qdisc mq state UP group default qlen 1000
link/ether xxxxxx brd ff:ff:ff:ff:ff:ff
altname enx500a52087489
inet 192.168.8.145/24 brd 192.168.8.255 scope global dynamic noprefixroute enp2s0
valid_lft 27083sec preferred_lft 27083sec
inet6 fe80::b7ea:8784:599d:506e/64 scope link noprefixroute
valid_lft forever preferred_lft forever
我们再来看一下路由表,使用 route -n 或者 ip route 来查看:
1
2
3
4
5
6
7
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.8.1 0.0.0.0 UG 100 0 0 enp2s0
172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 br-0da48190134d
......
172.100.0.0 0.0.0.0 255.255.255.0 U 0 0 0 docker0
192.168.8.0 0.0.0.0 255.255.255.0 U 100 0 0 enp2s0
路由表决定了数据包往哪里走。
第一条 0.0.0.0/0 是默认路由,所有未知目的地的数据包都会被转发到网关 192.168.8.1(你的路由器)。路由器会通过 NAPT 来实现多台设备共用一个公网 IP 访问互联网。最后一条 192.168.8.0/24 是局域网直连,不需要经过网关。172.100.0.0/24 是 Docker 网桥。
NAPT(Network Address Port Translation)是 NAT 的一种,
也叫 PAT(Port Address Translation),它允许多个内部主机共享一个公网 IP 地址,
通过不同的端口号来区分不同的连接。
你家路由器做的其实是 NAPT:
192.168.1.100:12345 → 公网IP:50001
192.168.1.101:12345 → 公网IP:50002
192.168.1.102:12345 → 公网IP:50003
如果你家里恰好有公网 IP,那么也可以通过在路由器上配置端口映射,将外部端口映射到内部的 IP 地址和端口上,让 NAS 可以被外网访问。
比如,你想让 NAS 的 SSH 服务(默认端口 22)可以从外网访问,可以在路由器上配置端口映射,将路由器的端口(比如 2222)映射到 NAS 的 IP 地址(比如 192.168.1.100)的 22 端口上。
不过懒猫微服自带了内网穿透,我们可以不用折腾这个部分了。
一般来说,家庭路由器的 IP 是 192.168.X.1,所以我们的机器可能是 192.168.X.2 或者其它地址。也就是说 192.168.X.0/24 就是我们局域网的网段。
192.168开头的是C类IP地址,后面/24表示子网掩码,也就是255.255.255.0
二进制表示就是 11111111.11111111.11111111.00000000
也就是前24位是网络号,后8位是主机号。
一个子网内,主机号不能全为0或者全为1
192.168.1.0 网络地址(全0)
192.168.1.255 广播地址(全1)
192.168.1.1-192.168.1.254 可用地址
如果你的设备足够多,200 多个可能不太够用,那么就需要用更大的网段,比如 172.16.0.0/12,也就是 172.16.0.0 - 172.31.255.255 可用地址。
你也可能看到过 10 开头的地址,这是 A 类私有地址,范围更大:10.0.0.0 - 10.255.255.255。
好了,回到 DNS(Domain Name System)。它是互联网的核心服务之一,作为域名和 IP 地址相互映射的分布式数据库,让我们可以通过域名来访问网站,而不用记一串数字。由于懒猫做了转发,实际看到的不是真正的 IP 地址,这里我们快速略过。
在局域网内也可以用 dig 查看 IPv4 地址:
1
dig +short your-lazycat.local
这里看到的地址就和路由器上显示的一样了。
如果想要使用IPv6,那么你需要确保你的网络环境支持IPv6,并且你的路由器也支持IPv6。你可以通过以下命令来查看你的IPv6地址:
1
ip addr show | grep inet6
因为 IPv4 地址已枯竭且存在 NAT 性能损耗,现代互联网依据 RFC 6724 国际标准和 Happy Eyeballs 算法,在操作系统和浏览器底层默认赋予了 IPv6 更高的连接优先级(Precedence),旨在确保连接更直接、高效的同时,通过给 IPv6 几十毫秒的“起跑优势”来实现全球网络向下一代协议的平滑过渡。
下载(curl wget)
那我们如果想从微服的终端下载东西呢,有 curl 和 wget。通常来讲,curl 用来请求 API,比如测试接口,如果你的懒猫微服里发布了 API 可以用 curl 简单地测试;而 wget 主要用来下载文件。直接在微服上下载,比先下到电脑再 SFTP 传过去快多了。
1
2
3
curl -X POST https://api.example.com/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "123"}'
1
wget https://example.com/file.zip
好了,接下来就是端口。
端口是一个逻辑概念,用于标识计算机上的不同服务。每个服务都会监听特定的端口,当有请求到达该端口时,系统会将请求转发给对应的服务。例如,HTTP 服务通常监听 80 端口,HTTPS 服务监听 443 端口,SSH 服务监听 22 端口。
在懒猫微服中,由于服务大多运行在容器中,所以似乎只有使用 pg-docker 的时候才会注意到端口,或者在终端里临时启动 web server:
1
2
lzcbox-e66ccc4a ~ # python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
那么我们如何知道哪些端口被占用呢?可以使用 netstat 或 ss 命令来查看。懒猫微服上容器多、服务杂,有时候端口冲突了,用这俩命令一查便知。我通常使用 -nltp:
参数解释:
-n 显示数字端口(不解析服务名)
-l 只看监听状态
-t 只看 TCP
-p 显示进程信息
1
2
ss -nltp | grep 8000
netstat -nltp | grep 8000
1
2
lzcbox-e66ccc4a ~ # ss -nltp | grep 8000
LISTEN 0 5 0.0.0.0:8000 0.0.0.0:* users:(("python",pid=618243,fd=3))
日常用 ss 就行,更快。老系统或习惯了 netstat 也没问题,结果基本一样。
你可能也会看到其他人使用 lsof,同样可以查端口。lsof 除了查询端口之外,还能查询文件、进程等,这里不再赘述。
1
2
apt update && apt install lsof
lsof -i :8000
测试远程端口:telnet 和 nc
知道端口被谁占了,下一步就是测试远程端口通不通。比如你在懒猫微服上起了个服务,想从电脑上确认能不能连上。
telnet 是最经典的方式:
1
2
3
4
5
6
7
apt install telnet
lzcbox-e66ccc4a ~ # telnet 192.168.8.1 22
Trying 192.168.8.1...
Connected to 192.168.8.1.
Escape character is '^]'.
SSH-2.0-dropbear
这就是 telnet 的用处——能看到服务返回的 banner 信息,做简单的服务识别。退出方式:按 Ctrl + ] 进入命令模式,然后输入 quit。
nc(netcat)更灵活,可以批量扫描端口:
1
2
3
4
5
nc -zv 192.168.8.1 20-100 2>&1 | grep succeeded
Connection to 192.168.8.1 22 port [tcp/ssh] succeeded!
Connection to 192.168.8.1 53 port [tcp/domain] succeeded!
Connection to 192.168.8.1 80 port [tcp/http] succeeded!
路径追踪:mtr 和 traceroute
端口通了,但网络还是慢?那就要看看路径上哪一跳出了问题。比如你从懒猫微服访问外网资源很慢,想知道是家里路由器的问题还是运营商的问题。
1
apt install mtr traceroute
网络不通的时候,光 ping 只能告诉你”不通”,但不知道卡在哪一跳。这时候就需要 traceroute 或 mtr。
traceroute 是单次探测,跑一遍就结束:
1
traceroute baidu.com
mtr 更强,结合了 ping 和 traceroute,持续发包,实时显示每一跳的丢包率和延迟:
1
mtr baidu.com
网络时好时坏的时候,traceroute 可能刚好跑的时候没问题就抓不到,mtr 跑几分钟就能看到哪一跳在丢包。日常排查推荐用 mtr。
抓包:tshark
和 tcpdump 相比,tshark 的协议解析能力更强,能直接看到 HTTP 请求内容,而 tcpdump 只能看到十六进制。
懒猫微服上跑着各种服务,有时候想看看某个应用到底在发什么请求、收什么响应,tshark 就派上用场了。
懒猫的系统环境如果默认没装 tshark,需要使用这个命令进行安装
1
apt update && apt install -y tshark
用 tshark 抓一个本地 HTTP 请求,看看 TCP/HTTP 到底是怎么通信的。先在一个终端启动抓包:
1
2
# 抓 lo 回环接口(本机访问本机走这里)
tshark -i lo -f "port 8000"
然后另一个终端访问 curl localhost:8000,抓到 12 个包:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
tshark -i lo -f "port 8000"
Running as user "root" and group "root". This could be dangerous.
Capturing on 'Loopback: lo'
** (tshark:1297291) 16:39:23.579519 [Main MESSAGE] -- Capture started.
** (tshark:1297291) 16:39:23.579553 [Main MESSAGE] -- File: "/tmp/wireshark_loK51JK3.pcapng"
1 0.000000000 192.168.8.145 → 192.168.8.145 TCP 74 59548 → 8000 [SYN] Seq=0 Win=65495 Len=0 MSS=65495 SACK_PERM TSval=665319807 TSecr=0 WS=1024
2 0.000007817 192.168.8.145 → 192.168.8.145 TCP 74 8000 → 59548 [SYN, ACK] Seq=0 Ack=1 Win=65483 Len=0 MSS=65495 SACK_PERM TSval=665319807 TSecr=665319807 WS=1024
3 0.000012956 192.168.8.145 → 192.168.8.145 TCP 66 59548 → 8000 [ACK] Seq=1 Ack=1 Win=65536 Len=0 TSval=665319807 TSecr=665319807
4 0.000035036 192.168.8.145 → 192.168.8.145 HTTP 148 GET / HTTP/1.1
5 0.000036994 192.168.8.145 → 192.168.8.145 TCP 66 8000 → 59548 [ACK] Seq=1 Ack=83 Win=65536 Len=0 TSval=665319807 TSecr=665319807
6 0.000801645 192.168.8.145 → 192.168.8.145 TCP 221 HTTP/1.0 200 OK [TCP segment of a reassembled PDU]
7 0.000813247 192.168.8.145 → 192.168.8.145 TCP 66 59548 → 8000 [ACK] Seq=83 Ack=156 Win=65536 Len=0 TSval=665319808 TSecr=665319808
8 0.000834148 192.168.8.145 → 192.168.8.145 HTTP 976 HTTP/1.0 200 OK (text/html)
9 0.000837352 192.168.8.145 → 192.168.8.145 TCP 66 59548 → 8000 [ACK] Seq=83 Ack=1066 Win=65536 Len=0 TSval=665319808 TSecr=665319808
10 0.000857121 192.168.8.145 → 192.168.8.145 TCP 66 8000 → 59548 [FIN, ACK] Seq=1066 Ack=83 Win=65536 Len=0 TSval=665319808 TSecr=665319808
11 0.000901652 192.168.8.145 → 192.168.8.145 TCP 66 59548 → 8000 [FIN, ACK] Seq=83 Ack=1067 Win=65536 Len=0 TSval=665319808 TSecr=665319808
12 0.000914845 192.168.8.145 → 192.168.8.145 TCP 66 8000 → 59548 [ACK] Seq=1067 Ack=84 Win=65536 Len=0 TSval=665319808 TSecr=665319808
针对上图的结果,我也总结了一个流程图,请看:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
客户端 (59548) 服务端 (8000)
| |
| -------- [SYN] ---------> | 1. 请求连接
| <----- [SYN,ACK] -------- | 2. 同意连接
| -------- [ACK] ---------> | 3. 确认
| |
| ===== 连接建立 ===== |
| |
| ---- GET / HTTP/1.1 ----> | 4. 发请求
| <------- [ACK] ---------- | 5. 确认
| <---- 200 OK + HTML ----- | 6-8. 返回数据
| -------- [ACK] ---------> | 9. 确认
| |
| ===== 数据传完 ===== |
| |
| <------ [FIN,ACK] ------- | 10. 服务端关闭
| ------- [FIN,ACK] ------> | 11. 客户端关闭
| <-------- [ACK] --------- | 12. 最终确认
| |
| ===== 连接关闭 ===== |
TCP 三次握手(建立连接)
1
2
3
1 59548 → 8000 [SYN] "我想连接你"
2 8000 → 59548 [SYN,ACK] "好的,我也准备好了"
3 59548 → 8000 [ACK] "收到,开始通信"
为什么要三次?确保双方都能收发。两次不够——服务端不知道客户端能不能收到它的回复。
HTTP 请求/响应
1
2
3
4
4 GET / HTTP/1.1 客户端请求首页
5 [ACK] 服务端确认收到
6-8 HTTP/1.0 200 OK 服务端返回网页
9 [ACK] 客户端确认收到
TCP 四次挥手(关闭连接)
1
2
3
10 [FIN,ACK] 服务端 → 客户端 "我发完了"
11 [FIN,ACK] 客户端 → 服务端 "我也发完了"
12 [ACK] 服务端 → 客户端 "好的,再见"
虽然抓包看只有三行,但逻辑上它们依然是四次挥手的变体。
一共 12 个包,一次完整的 HTTP 请求就完成了。
尾声
网络之道,说繁亦繁,说简亦简。繁在协议层层叠叠,简在万变不离其宗——包从哪来,到哪去。
能用 tshark 看懂三次握手,用 mtr 定位丢包,用 ss 追溯端口归属,便已胜过大多数人。
所谓高手,不过是在无数次折腾中,把书本上的知识化作了指尖的肌肉记忆。
而懒猫微服,恰是这样一方天地——自带穿透,容器隔离,root 权限在握,尽可放手施为。纵有闪失,重置便是,不伤筋骨。
这大概就是它最大的魅力:不只是一台 NAS,更是折腾 infra、钻研安全的瑞士军刀。
所有的好奇心,皆有安放之处。
---
## 懒猫微服:不只是 NAS,更适合云计算宝宝体质
- URL: https://blog.no-claw.com/posts/b0a96c84/
- 日期: 2026-01-16
- 分类: 懒猫微服 / 番外
- 标签: 懒猫微服, NAS
提到 NAS,大多数人的第一反应是”存储设备”——用来备份照片、存电影、共享文件。但懒猫微服想做的,远不止于此。
在云计算时代,个人和小团队同样需要强大的计算能力、灵活的开发环境和丰富的应用生态。懒猫微服正是为此而生,它不仅能存储数据,更能像云服务器一样运行虚拟机、部署应用、搭建开发环境。从底层的虚拟化基础设施,到中间层的平台服务,再到上层的软件应用,懒猫微服构建了一个完整的私有云生态。
云计算三层架构:IaaS、PaaS、SaaS
云计算的核心架构分为三层:IaaS(Infrastructure as a Service,基础设施即服务)提供虚拟化的计算、存储和网络资源;PaaS(Platform as a Service,平台即服务)在 IaaS 之上提供应用运行平台和开发工具;SaaS(Software as a Service,软件即服务)则提供开箱即用的应用软件。
用一个形象的比喻:IaaS 像是租了一块地和建筑材料,你要自己盖房子、装修、买家具;PaaS 像是租了一套毛坯房,你只需要装修和买家具;SaaS 则像是租了一套精装修的酒店房间,拎包入住。
懒猫微服正是基于这一架构理念,构建了完整的私有云计算平台。接下来,我们将深入探讨懒猫微服在这三个层面的技术实现。
懒猫微服的 IaaS 实现:虚拟化基础设施
IaaS 层的技术本质
让我们从最底层说起。IaaS 提供的是虚拟化的计算基础设施——虚拟机实例、块存储(Block Storage)、对象存储(Object Storage)、虚拟网络(VPC)、负载均衡器。这意味着什么?意味着你拥有了完整的基础设施控制权,但也需要承担相应的管理责任:操作系统、中间件、应用程序的安装配置,系统更新、安全补丁、网络策略的维护。AWS EC2、阿里云 ECS、Azure Virtual Machines 走的都是这条路。
懒猫微服的虚拟机能力
懒猫微服在 IaaS 层做了什么?我们集成了 WebVirtCloud 虚拟化管理平台,让你可以轻松创建和管理各种操作系统的虚拟机。Windows 轻办公?没问题。各类 Linux 发行版(Ubuntu、Debian、CentOS、Arch Linux)做服务器?当然可以。想要 macOS 开发环境?甚至 Android 移动应用测试?统统支持。
更重要的是,我们在商店里内置了大量预配置的系统镜像。你不需要到处找 ISO 文件,不需要手动配置,就像安装手机 App 一样点击安装,虚拟机就跑起来了。这才是真正的开箱即用。
有意思的是,社区开发者还上传了群晖(Synology DSM)的虚拟机镜像。是的,你可以在懒猫微服里运行群晖系统——NAS 嵌套 NAS,这种玩法让你可以对比测试不同 NAS 系统的特性。
虚拟化技术架构
说到虚拟化,我们需要理解两种根本不同的架构。
Type 1 Hypervisor(裸金属虚拟化) 直接运行在物理硬件之上,Hypervisor 直接掌控硬件资源。这是性能、资源利用率、安全隔离性的最优解,也是企业级数据中心和云服务提供商的标准选择。VMware ESXi、Xen、KVM、Microsoft Hyper-V Server 都属于这一阵营。
Type 2 Hypervisor(寄居虚拟化) 则运行在宿主操作系统之上,作为应用程序层存在。安装配置简单,易于使用,这是它的优势。性能?确实略逊于 Type 1,但对个人用户和开发测试场景来说,易用性和灵活性更重要。VMware Workstation、Oracle VirtualBox、Parallels Desktop 走的是这条路。
那么懒猫微服选择了什么? 我们采用 Type 2 架构,但不止于此。团队基于精简的 Linux 内核进行了大量性能调优,减少虚拟化层的开销,让性能表现接近 Type 1。这意味着什么?你既能获得高性能的虚拟化能力,又无需复杂的裸金属部署和配置。这才是真正适合个人和小团队的技术路线。
容器化技术实现
虚拟机很强大,但有时候你需要更轻量级的方案。这就是容器的价值所在。
懒猫微服基于 Docker 容器引擎,构建了三层容器管理架构。让我们逐层看看:
系统级 Docker(System-level Container Runtime) 运行着懒猫微服的核心系统组件和基础服务。这一层对你透明,系统自动管理,采用资源隔离和安全沙箱机制,确保核心服务的稳定性不受任何用户操作影响。
Playground Docker(开发测试环境) 这是你的实验场。支持 lzc-docker 和 Dockge 两种管理工具,你可以快速创建、销毁容器实例。想测试新版本数据库?想体验最新的开源项目?在 Playground 里随便折腾,完全隔离于生产环境,不会影响系统稳定性。
商店 Docker(容器商店) 这里的应用都经过我们工程师的专门审核,支持版本更新和一键部署。涵盖了你能想到的几乎所有中间件和应用:数据库服务(MySQL、PostgreSQL、MongoDB、Redis、Milvus 向量数据库)、搜索引擎(Elasticsearch、Meilisearch)、Web 服务器(Nginx、Apache、Caddy)、数据库管理工具(Adminer、phpMyAdmin、MongoDB Compass)、AI 大语言模型平台(Ollama、Ollama WebUI)、工作流自动化(n8n、Dify)。
虚拟机和容器,重量级隔离和轻量级部署,懒猫微服给了你完整的技术栈。虚拟机适合运行复杂应用和多租户场景,容器适合微服务架构和快速迭代。选择权在你手里。
技术应用场景: 独立开发者可以利用懒猫微服搭建 Kubernetes 集群学习环境。传统方案需要购买多台云服务器(每月数百元成本),而在懒猫微服上只需创建 3-5 个 Linux 虚拟机,部署 K8s 控制平面和工作节点,即可在本地环境进行容器编排学习和实验,零云服务成本。对于需要 macOS 开发环境但缺少 Mac 硬件的开发者,也可以通过虚拟化 macOS 系统进行应用开发和测试。
懒猫微服的 PaaS 实现:平台服务层
PaaS 层的技术定位
PaaS 在 IaaS 之上提供了更高层次的抽象,封装了应用运行所需的平台环境和开发工具。在 PaaS 层,用户无需关心操作系统补丁、运行时环境配置、负载均衡策略等底层细节,只需专注于应用代码开发和业务逻辑实现。PaaS 通常提供预配置的运行时环境(Node.js、Python、Java、Go 等)、托管数据库服务(自动备份、主从复制、故障转移)、CI/CD 流水线、API 网关、服务网格等能力。Heroku、Google App Engine、Azure App Service 是典型的 PaaS 产品。
懒猫微服的平台服务能力
懒猫微服在 PaaS 层构建了完整的平台服务体系,涵盖了应用托管、数据存储、服务治理等多个维度。
统一服务门户(Service Portal) 懒猫微服提供了平台级的服务管理能力,用于统一管理和访问 PaaS 层的各类技术服务(数据库、中间件、DevOps 工具等)。管理员可以通过服务门户进行服务配置、权限控制、资源监控等操作。此外,懒猫微服还支持静态网站托管服务,用户可以部署基于 HTML/CSS/JavaScript 的静态网站、单页应用(SPA)、技术文档站点等,无需配置 Web 服务器。
对象存储服务(Object Storage) 懒猫微服集成了 MinIO 和 RustFS 企业级对象存储系统,提供与 AWS S3 兼容的 API 接口。MinIO 支持分布式部署、数据冗余、版本控制、生命周期管理等企业级特性。用户可以通过 S3 SDK 或 MinIO Client 进行对象存储操作,适用于图片/视频存储、数据备份归档、大文件分发等场景。MinIO 的高性能架构可以充分利用本地存储的 I/O 能力,提供低延迟的数据访问。
数据库服务(Database as a Service) 懒猫微服提供了多种托管数据库服务,包括关系型数据库(MySQL、PostgreSQL)、NoSQL 数据库(MongoDB、Redis)、时序数据库(InfluxDB)等。这些数据库服务支持一键部署、自动备份、性能监控等功能,用户无需手动配置数据库参数和优化策略。
DevOps 工具链 懒猫微服集成了完整的 DevOps 工具栈,包括代码仓库(GitLab、Gitea)、CI/CD 平台(Jenkins、GitLab Runner)、制品仓库(Nexus、Harbor)。开发团队可以在懒猫微服上搭建完整的软件交付流水线,实现代码提交、自动构建、测试、部署的全流程自动化。
监控与可观测性(Observability Stack) 提供了 Prometheus + Grafana 监控方案、ELK(Elasticsearch + Logstash + Kibana)日志分析栈。用户可以根据需求实时监控系统资源使用情况、应用性能指标、业务数据趋势,快速定位和排查问题。
技术应用场景: 小型技术团队(3-5人)需要搭建开发基础设施。传统方案需要运维工程师花费数天时间采购服务器、安装操作系统、配置网络、部署各类中间件。而在懒猫微服上,技术负责人只需在应用商店中依次安装:GitLab(代码管理)、Jenkins(CI/CD)、PostgreSQL(业务数据库)、Redis(缓存)、Grafana(监控面板),配置导航页统一入口,整个过程可在 30 分钟内完成。团队成员通过导航页即可访问所有开发工具,大幅提升协作效率,节省了服务器采购和运维成本。
懒猫微服的 SaaS 实现:应用软件生态
SaaS 层的服务模式
SaaS 是云计算的最上层,向最终用户提供开箱即用的应用软件。在 SaaS 模式下,软件的安装、配置、更新、备份、安全防护等工作全部由服务提供商负责,用户只需通过浏览器或客户端访问应用,专注于使用软件功能完成业务目标。SaaS 应用通常采用多租户架构(Multi-tenancy),支持按需订阅、弹性计费、数据隔离等特性。Gmail、Microsoft 365、Salesforce、Slack、Notion 都是典型的 SaaS 产品。
懒猫微服的应用生态
懒猫微服在 SaaS 层构建了丰富的应用生态,涵盖了个人生产力、协作办公、智能家居、安全管理、娱乐休闲等多个领域。
官方应用套件 懒猫微服提供了一系列官方开发的核心应用:懒猫网盘(基于 WebDAV 协议的私有云存储,支持跨平台同步)、相册管理(集成 AI 图像识别,支持人脸识别、场景分类、智能搜索)、Todo 清单(任务管理和 GTD 工作流)、智慧屏(信息聚合展示,支持自定义 Widget)。这些应用覆盖了个人用户的日常办公和生活需求,提供了完整的数据主权和隐私保护。
导航页应用生态 在 SaaS 层,懒猫微服支持多种导航页应用,用户可以根据个人喜好选择和配置。这些导航页应用需要用户自行安装和配置,用于管理个人书签、快速访问常用网站和服务。包括 LazyCat 导航(官方导航页)、HomeNexus 导航(现代化服务导航面板)、T-Nav 导航网站(轻量级导航工具)、Van-nav(轻量级导航站)、Nav8(现代化个人导航页)、Sun-Panel(NAS 导航面板)、BookNav(基于 Flask 的书签导航)、Flare(个人导航、快速、美观的个人导航页)、Catsite(美观实用的个人导航)、Easy Gate(开箱即用的导航管理)、homepage(高度可定制的导航页)等。每个导航页应用都有不同的设计风格和功能特点,用户可以根据自己的审美和使用习惯进行选择和定制。
智能家居与自动化 懒猫微服支持 Home Assistant(开源智能家居平台,支持数千种智能设备接入,提供自动化场景编排、设备联动、语音控制等功能)。用户可以在懒猫微服上搭建完整的智能家居中枢,实现本地化的设备控制和数据隐私保护,无需依赖云服务商。
安全与隐私工具 集成了 Bitwarden(开源密码管理器,支持端到端加密、多设备同步、密码生成、安全审计等功能)。用户可以在懒猫微服上自建密码管理服务,完全掌控敏感凭证数据,避免第三方密码管理服务的潜在风险。
社区开发者生态 懒猫微服拥有活跃的开发者社区,第三方开发者贡献了大量高质量应用。音乐播放器(支持本地音乐库管理和流媒体播放)、Rustdesk 远程桌面服务端(开源的 TeamViewer 替代方案,提供端到端加密的远程访问)、微信排版工具(Markdown 编辑器,支持微信公众号格式导出)、Office 办公套件(基于 OnlyOffice 或 Collabora Online 的在线文档编辑)等应用,极大扩展了懒猫微服的应用场景。
游戏娱乐平台 懒猫微服还支持游戏应用的部署。包括 Web 小游戏、PSP 模拟器、甚至移植的 3A 游戏大作。用户可以在懒猫微服上构建个人游戏库,通过流式传输技术在不同设备上游玩。
技术应用场景: 摄影爱好者拥有数万张照片(总容量超过 500GB),需要一个私密且便捷的管理方案。使用公有云存储服务(如 iCloud、Google Photos)面临隐私风险和持续订阅成本。在懒猫微服上部署懒猫网盘和相册应用后,可以实现照片的自动备份(通过移动端 App 或 WebDAV 协议)、AI 智能分类(人脸识别、场景标签)、全文搜索、跨设备访问。数据完全存储在本地,无隐私泄露风险,也无需支付月度订阅费用。家庭成员可以通过权限管理共享相册,实现家庭照片的集中管理和协作浏览。
总结:属于你的私有云时代
懒猫微服不仅仅是一个 NAS 存储设备,更是一个完整的云计算平台。它打破了云计算只属于大企业的刻板印象,让个人用户、独立开发者、小团队也能拥有媲美云服务商的基础设施能力。
从底层的虚拟化基础设施(IaaS),到中间层的平台服务(PaaS),再到上层的软件应用(SaaS),懒猫微服为用户提供了全栈的云计算解决方案。你不需要每月支付高昂的云服务费用,不需要担心数据隐私泄露,不需要受限于云服务商的各种限制。一台懒猫微服,就是你的私有云数据中心。
无论你是想搭建个人博客、学习新技术、开发应用、管理家庭数据,还是为小团队提供开发环境,懒猫微服都能满足你的需求。云计算不再遥不可及,它就在你的桌面上,触手可及。
这就是懒猫微服的愿景:让每个人都能拥有自己的云计算平台,让技术真正为生活和工作赋能。
---
## Python小抄们
- URL: https://blog.no-claw.com/posts/11ca0dba/
- 日期: 2025-12-31
- 分类: 软件
- 标签: Python
git clone –depth 1 是浅克隆(shallow clone),只拉取最近一次提交的历史记录,而不是整个仓库的完整历史。
主要好处:
下载速度快,节省带宽和磁盘空间
适合只想获取最新代码、不关心历史记录的场景
比如一个有几千次提交的大仓库,完整克隆可能要几百 MB,用 –depth 1 可能只需要几十 MB。
常见用法:
只拉最新一次提交
git clone –depth 1 https://github.com/user/repo.git
拉最近 10 次提交
git clone –depth 10 https://github.com/user/repo.git
缺点是你看不到完整的 commit 历史,也没法切换到旧的分支或标签。如果后续需要完整历史,可以用 git fetch –unshallow 补全。
---
## 装修日记
- URL: https://blog.no-claw.com/posts/a7fd58f1/
- 日期: 2025-12-31
- 分类: 零碎生活
- 标签: 生活
卫浴能买九牧不要买四季沐歌,毕竟不是做一条产品线起家的。四季沐歌虽然听着耳熟,但是最早是做太阳能的。
厨房的电闸要多留一些冗余,16A不太够用,烧水壶+冰箱+油烟机+饭锅 就小3000W了,如果再有电锅啥的可能直接跳闸。
三个连排的插座额定功率也是10A,所以也没有那么方便,大用电器容器超。
---
## 告别 DSL!我用 MCP 让 AI 帮我操作 Easysearch
- URL: https://blog.no-claw.com/posts/9ca6531a/
- 日期: 2025-12-30
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
还在为记不住 Elasticsearch DSL 语法发愁?match 和 term 傻傻分不清?聚合查询写半天还报错?
今天教你用 MCP(Model Context Protocol)封装 Easysearch,让 AI 帮你操作集群。从此告别 DSL,用自然语言就能完成搜索、写入、聚合统计!
效果演示
先看看最终效果。以下是我和 Kiro 的真实对话记录:
我:集群健康吗?
Kiro:
1
2
3
4
5
6
7
{
"cluster_name": "infinilabs",
"status": "green",
"number_of_nodes": 1,
"active_shards": 2,
"active_shards_percent_as_number": 100.0
}
🟢 集群很健康!状态 green,1 个节点,2 个活跃分片,100% 健康。
我:创建一个产品索引,有名称、价格、分类、描述字段
Kiro:
1
2
3
4
5
{
"acknowledged": true,
"shards_acknowledged": true,
"index": "products"
}
产品索引创建成功!字段结构:
字段
类型
说明
name
text
产品名称,支持中文分词搜索
price
integer
价格
category
keyword
分类,精确匹配
description
text
描述,支持中文分词搜索
我:写入 5 个苹果产品
Kiro:
1
2
3
4
5
{
"took": 23,
"errors": false,
"items_count": 5
}
5 个产品写入成功!
我:搜一下带芯片的产品
Kiro:
1
2
3
4
5
6
7
8
9
{
"total": 3,
"took_ms": 37,
"hits": [
{"_source": {"name": "iPad Air", "price": 4799, "category": "平板", "description": "M1芯片平板电脑"}},
{"_source": {"name": "MacBook Pro 14", "price": 16999, "category": "电脑", "description": "M3 Pro芯片笔记本电脑"}},
{"_source": {"name": "iPhone 15 Pro", "price": 8999, "category": "手机", "description": "苹果最新旗舰手机,A17芯片"}}
]
}
找到 3 个带芯片的产品!
我:5000 以下的有哪些?
Kiro:
1
2
3
4
5
6
7
{
"total": 2,
"hits": [
{"_source": {"name": "AirPods Pro 2", "price": 1899, "category": "耳机"}},
{"_source": {"name": "iPad Air", "price": 4799, "category": "平板"}}
]
}
5000 以下有 2 个:AirPods Pro 2(¥1899)和 iPad Air(¥4799)
我:按分类统计数量
Kiro:
1
2
3
4
5
6
7
8
9
{
"buckets": [
{"key": "平板", "count": 1},
{"key": "手机", "count": 1},
{"key": "手表", "count": 1},
{"key": "电脑", "count": 1},
{"key": "耳机", "count": 1}
]
}
我:平均价格多少?
Kiro:
1
{"value": 7839.0}
平均价格 ¥7839
全程不用写一行 DSL!我只说人话,Kiro 自动选择合适的工具、生成查询、返回结果。
什么是 MCP?
MCP(Model Context Protocol)是一个让 AI 调用外部工具的协议。简单说:
你定义一些”工具”(函数)
AI 根据用户意图选择合适的工具
AI 自动填参数、调用、返回结果
把 Easysearch 的操作封装成 MCP 工具,AI 就能帮你操作集群了。
为什么用 FastMCP?
FastMCP 是 MCP 官方提供的 Python 高级封装,让你用最少的代码写 MCP Server。
1
2
3
4
5
6
7
8
9
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("easysearch")
@mcp.tool()
def cluster_health() -> dict:
"""获取集群健康状态"""
# 实现逻辑...
return result
FastMCP 的优势:
装饰器语法 - @mcp.tool() 一行搞定工具注册
自动生成 Schema - 根据函数签名和类型注解自动生成参数定义
docstring 即描述 - 函数文档字符串自动变成工具描述,AI 根据这个选择调用哪个工具
同步函数支持 - 不用写 async/await
返回值自动序列化 - 直接 return dict,不用手动包装成 JSON
开始封装
项目结构
1
2
3
4
easysearch-mcp-server/
├── easysearch_mcp.py # MCP 服务器代码
├── pyproject.toml # 项目配置
└── README.md
安装依赖
1
pip install mcp httpx
核心代码
创建 easysearch_mcp.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
"""
Easysearch MCP Server
让 AI Agent 能够操作 Easysearch(兼容 Elasticsearch API)
"""
import json
import os
from typing import Any
import httpx
from mcp.server.fastmcp import FastMCP
# 创建 MCP Server
mcp = FastMCP("easysearch")
# 配置 - 从环境变量读取
EASYSEARCH_URL = os.getenv("EASYSEARCH_URL", "http://localhost:9200")
EASYSEARCH_USER = os.getenv("EASYSEARCH_USER", "admin")
EASYSEARCH_PASSWORD = os.getenv("EASYSEARCH_PASSWORD", "admin")
def get_client() -> httpx.Client:
"""创建 HTTP 客户端"""
return httpx.Client(
base_url=EASYSEARCH_URL,
auth=(EASYSEARCH_USER, EASYSEARCH_PASSWORD),
verify=False, # 如果用 HTTPS 自签名证书
timeout=30.0
)
封装集群信息工具
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@mcp.tool()
def cluster_health() -> dict:
"""
获取集群健康状态
返回集群名称、状态(green/yellow/red)、节点数、分片数等信息
"""
with get_client() as client:
r = client.get("/_cluster/health")
return r.json()
@mcp.tool()
def cluster_stats() -> dict:
"""
获取集群统计信息
包括文档数、存储大小、索引数量等
"""
with get_client() as client:
r = client.get("/_cluster/stats")
data = r.json()
# 精简返回,避免太长
return {
"cluster_name": data.get("cluster_name"),
"status": data.get("status"),
"nodes": data.get("nodes", {}).get("count", {}),
"indices": {
"count": data.get("indices", {}).get("count"),
"docs": data.get("indices", {}).get("docs", {}),
"store_size": data.get("indices", {}).get("store", {}).get("size_in_bytes")
}
}
封装索引操作工具
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@mcp.tool()
def list_indices() -> list:
"""
列出所有索引
返回索引名称、文档数、存储大小、健康状态
"""
with get_client() as client:
r = client.get("/_cat/indices?format=json")
indices = r.json()
return [{
"index": idx.get("index"),
"health": idx.get("health"),
"status": idx.get("status"),
"docs_count": idx.get("docs.count"),
"store_size": idx.get("store.size")
} for idx in indices if not idx.get("index", "").startswith(".")]
@mcp.tool()
def get_index_mapping(index: str) -> dict:
"""
获取索引的字段映射(schema)
参数:
index: 索引名称
"""
with get_client() as client:
r = client.get(f"/{index}/_mapping")
return r.json()
@mcp.tool()
def create_index(index: str, mappings: dict = None, settings: dict = None) -> dict:
"""
创建新索引
参数:
index: 索引名称
mappings: 字段映射定义(可选)
settings: 索引设置如分片数(可选)
示例 mappings:
{"properties": {"title": {"type": "text"}, "count": {"type": "integer"}}}
"""
body = {}
if mappings:
body["mappings"] = mappings
if settings:
body["settings"] = settings
with get_client() as client:
r = client.put(f"/{index}", json=body if body else None)
return r.json()
@mcp.tool()
def delete_index(index: str) -> dict:
"""
删除索引(危险操作,会删除所有数据)
参数:
index: 要删除的索引名称
"""
with get_client() as client:
r = client.delete(f"/{index}")
return r.json()
封装文档操作工具
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@mcp.tool()
def index_document(index: str, document: dict, doc_id: str = None) -> dict:
"""
写入单个文档
参数:
index: 索引名称
document: 文档内容(JSON 对象)
doc_id: 文档 ID(可选,不传则自动生成)
"""
with get_client() as client:
if doc_id:
r = client.put(f"/{index}/_doc/{doc_id}", json=document)
else:
r = client.post(f"/{index}/_doc", json=document)
return r.json()
@mcp.tool()
def get_document(index: str, doc_id: str) -> dict:
"""
根据 ID 获取文档
参数:
index: 索引名称
doc_id: 文档 ID
"""
with get_client() as client:
r = client.get(f"/{index}/_doc/{doc_id}")
return r.json()
@mcp.tool()
def delete_document(index: str, doc_id: str) -> dict:
"""
删除单个文档
参数:
index: 索引名称
doc_id: 文档 ID
"""
with get_client() as client:
r = client.delete(f"/{index}/_doc/{doc_id}")
return r.json()
@mcp.tool()
def bulk_index(index: str, documents: list) -> dict:
"""
批量写入文档
参数:
index: 索引名称
documents: 文档列表
"""
lines = []
for doc in documents:
lines.append(json.dumps({"index": {"_index": index}}))
lines.append(json.dumps(doc))
body = "\n".join(lines) + "\n"
with get_client() as client:
r = client.post(
"/_bulk",
content=body,
headers={"Content-Type": "application/x-ndjson"}
)
result = r.json()
return {
"took": result.get("took"),
"errors": result.get("errors"),
"items_count": len(result.get("items", []))
}
封装搜索工具(重点!)
这是最有价值的部分,让 AI 帮你写 DSL:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@mcp.tool()
def search(index: str, query: dict, size: int = 10) -> dict:
"""
执行搜索查询
参数:
index: 索引名称(可用 * 搜索所有索引)
query: Elasticsearch DSL 查询
size: 返回结果数量,默认 10
示例 - 全文搜索:
search("products", {"match": {"name": "iPhone"}})
示例 - 精确匹配:
search("products", {"term": {"status": "active"}})
示例 - 范围查询:
search("products", {"range": {"price": {"gte": 100, "lte": 500}}})
"""
body = {
"query": query,
"size": size
}
with get_client() as client:
r = client.post(f"/{index}/_search", json=body)
result = r.json()
hits = result.get("hits", {})
return {
"total": hits.get("total", {}).get("value", 0),
"took_ms": result.get("took"),
"hits": [{
"_id": h.get("_id"),
"_score": h.get("_score"),
"_source": h.get("_source")
} for h in hits.get("hits", [])]
}
@mcp.tool()
def search_simple(index: str, keyword: str, field: str = "_all", size: int = 10) -> dict:
"""
简单关键词搜索(适合不熟悉 DSL 的场景)
参数:
index: 索引名称
keyword: 搜索关键词
field: 搜索字段,默认全字段
size: 返回数量
"""
if field == "_all":
query = {"query_string": {"query": keyword}}
else:
query = {"match": {field: keyword}}
return search(index, query, size)
封装聚合统计工具
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@mcp.tool()
def aggregate(index: str, field: str, agg_type: str = "terms", size: int = 10) -> dict:
"""
聚合统计
参数:
index: 索引名称
field: 聚合字段
agg_type: 聚合类型 - terms(分组计数), avg, sum, min, max, cardinality(去重计数)
size: 返回桶数量(仅 terms 有效)
"""
if agg_type == "terms":
agg_body = {"terms": {"field": field, "size": size}}
else:
agg_body = {agg_type: {"field": field}}
body = {
"size": 0,
"aggs": {"result": agg_body}
}
with get_client() as client:
r = client.post(f"/{index}/_search", json=body)
result = r.json()
agg_result = result.get("aggregations", {}).get("result", {})
if agg_type == "terms":
return {
"buckets": [{
"key": b.get("key"),
"count": b.get("doc_count")
} for b in agg_result.get("buckets", [])]
}
else:
return {"value": agg_result.get("value")}
启动入口
1
2
if __name__ == "__main__":
mcp.run()
配置 Kiro
在项目根目录创建 .kiro/settings/mcp.json:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"mcpServers": {
"easysearch": {
"command": "python",
"args": ["/path/to/easysearch_mcp.py"],
"env": {
"EASYSEARCH_URL": "https://localhost:9200",
"EASYSEARCH_USER": "admin",
"EASYSEARCH_PASSWORD": "your-password"
},
"autoApprove": [
"cluster_health",
"list_indices",
"cluster_stats",
"create_index",
"bulk_index",
"get_index_mapping",
"search_simple",
"search",
"aggregate",
"index_document",
"get_document",
"delete_document",
"delete_index"
]
}
}
}
配置说明:
command: Python 解释器路径,建议用绝对路径如 /usr/bin/python3
args: MCP 服务器脚本的绝对路径
env: 环境变量,配置 Easysearch 连接信息
autoApprove: 自动批准的工具列表。默认情况下 Kiro 调用 MCP 工具前会弹窗让你确认,把工具名加到这个列表里就会自动执行,不用每次点确认,对话更流畅
重新加载 MCP:Cmd+Shift+P → 搜索 “Reconnect MCP Servers” 执行即可。
连接成功后,在 Kiro 左侧面板的 “MCP Servers” 视图里可以看到 easysearch 显示为绿色。
更多对话示例
继续看几个实际操作:
我:有哪些索引?
Kiro:(调用 list_indices)
1
2
3
4
5
6
7
{
"index": "products",
"health": "yellow",
"status": "open",
"docs_count": "5",
"store_size": "5.7kb"
}
目前有 1 个索引 products,包含 5 个文档。
我:products 索引有什么字段?
Kiro:(调用 get_index_mapping)
1
2
3
4
5
6
7
8
9
10
11
12
{
"products": {
"mappings": {
"properties": {
"category": {"type": "keyword"},
"description": {"type": "text"},
"name": {"type": "text"},
"price": {"type": "integer"}
}
}
}
}
products 索引有 4 个字段:name、price、category、description。
我:集群有多少文档?
Kiro:(调用 cluster_stats)
1
2
3
4
5
6
7
8
9
{
"cluster_name": "infinilabs",
"status": "green",
"indices": {
"count": 3,
"docs": {"count": 13, "deleted": 0},
"store_size": 52841
}
}
集群共有 3 个索引,13 个文档,存储大小约 52KB。
我:写入单个文档
Kiro:(调用 index_document)
1
2
3
4
5
{
"_index": "products",
"_id": "magsafe-001",
"result": "created"
}
我:查一下 ID 是 magsafe-001 的文档
Kiro:(调用 get_document)
1
2
3
4
5
6
7
8
9
10
{
"_id": "magsafe-001",
"found": true,
"_source": {
"category": "配件",
"description": "MagSafe 磁吸无线充电器",
"name": "MagSafe 充电器",
"price": 329
}
}
我:搜一下手机分类的产品
Kiro:(调用 search,生成 term 查询)
1
2
3
4
5
6
{
"total": 1,
"hits": [
{"_source": {"name": "iPhone 15 Pro", "price": 8999, "category": "手机", "description": "苹果最新旗舰手机,A17芯片"}}
]
}
我:价格最高的是哪个?
Kiro:(调用 aggregate,agg_type=max)
1
{"value": 16999.0}
最高价格是 ¥16999(MacBook Pro 14)
我:价格最低呢?
Kiro:(调用 aggregate,agg_type=min)
1
{"value": 1899.0}
最低价格是 ¥1899(AirPods Pro 2)
我:算一下总价
Kiro:(调用 aggregate,agg_type=sum)
1
{"value": 39195.0}
5 个产品总价 ¥39195
我:删掉这个文档
Kiro:(调用 delete_document)
1
2
3
4
{
"_id": "magsafe-001",
"result": "deleted"
}
我:删掉 products 索引
Kiro:(调用 delete_index)
1
{"acknowledged": true}
索引已删除!
工具清单
封装完成后,AI 可以使用这 12 个工具:
工具
功能
典型用法
cluster_health
集群健康状态
“集群状态怎么样”
cluster_stats
集群统计
“有多少文档”
list_indices
列出索引
“有哪些索引”
get_index_mapping
查看字段结构
“products 索引有什么字段”
create_index
创建索引
“创建一个用户索引”
delete_index
删除索引
“删掉 test 索引”
index_document
写入文档
“添加一个产品”
get_document
获取文档
“查一下 ID 是 xxx 的文档”
delete_document
删除文档
“删掉这个文档”
bulk_index
批量写入
“导入这批数据”
search
DSL 搜索
“价格 1000-5000 的产品”
search_simple
关键词搜索
“搜一下 iPhone”
aggregate
聚合统计
“按分类统计”
设计要点
1. 工具描述要清晰
AI 根据工具描述选择调用哪个,描述写得好,AI 选得准:
1
2
3
4
5
6
7
8
9
@mcp.tool()
def search_simple(index: str, keyword: str, ...):
"""
简单关键词搜索(适合不熟悉 DSL 的场景) # 说明用途
参数:
index: 索引名称
keyword: 搜索关键词 # 参数说明
"""
2. 返回结果要精简
Easysearch 原始返回包含大量元数据,动辄几百行。直接返回给 AI 会占用太多 token,也会干扰理解。精简后只保留关键信息:
1
2
3
4
5
6
7
8
9
return {
"total": hits.get("total", {}).get("value", 0),
"took_ms": result.get("took"),
"hits": [{
"_id": h.get("_id"),
"_score": h.get("_score"),
"_source": h.get("_source")
} for h in hits.get("hits", [])]
}
3. 提供简化版工具
search 需要写 DSL,search_simple 只要关键词。AI 会根据场景选择:
用户说”搜 iPhone” → 用 search_simple
用户说”价格 1000-5000” → 用 search 生成 range 查询
扩展思路
这个 MCP 还可以继续扩展:
添加更多搜索类型:bool 组合查询、fuzzy 模糊搜索、highlight 高亮
索引管理:reindex、别名管理、模板管理
集群运维:节点信息、分片分配、慢查询日志
数据导入导出:从 CSV/JSON 文件批量导入
总结
通过 MCP 封装 Easysearch:
告别 DSL 记忆负担 - AI 帮你生成查询语句
自然语言交互 - 说人话就能操作集群
降低使用门槛 - 不懂 ES 的人也能用
提高效率 - 复杂查询秒出结果
完整代码已开源,拿去用吧!
附录:完整源码
Kiro MCP 配置文件
.kiro/settings/mcp.json:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"mcpServers": {
"easysearch": {
"command": "python",
"args": ["/path/to/easysearch_mcp.py"],
"env": {
"EASYSEARCH_URL": "https://localhost:9200",
"EASYSEARCH_USER": "admin",
"EASYSEARCH_PASSWORD": "your-password"
},
"autoApprove": [
"cluster_health",
"list_indices",
"cluster_stats",
"create_index",
"bulk_index",
"get_index_mapping",
"search_simple",
"search",
"aggregate",
"index_document",
"get_document",
"delete_document",
"delete_index"
]
}
}
}
MCP Server 完整源码
easysearch_mcp.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
"""
Easysearch MCP Server
让 AI Agent 能够操作 Easysearch(兼容 Elasticsearch API)
"""
import json
import os
from typing import Any
import httpx
from mcp.server.fastmcp import FastMCP
# 创建 MCP Server
mcp = FastMCP("easysearch")
# 配置 - 从环境变量读取
EASYSEARCH_URL = os.getenv("EASYSEARCH_URL", "http://localhost:9200")
EASYSEARCH_USER = os.getenv("EASYSEARCH_USER", "admin")
EASYSEARCH_PASSWORD = os.getenv("EASYSEARCH_PASSWORD", "admin")
def get_client() -> httpx.Client:
"""创建 HTTP 客户端"""
return httpx.Client(
base_url=EASYSEARCH_URL,
auth=(EASYSEARCH_USER, EASYSEARCH_PASSWORD),
verify=False, # 如果用 HTTPS 自签名证书
timeout=30.0
)
# ============ 集群信息 ============
@mcp.tool()
def cluster_health() -> dict:
"""
获取集群健康状态
返回集群名称、状态(green/yellow/red)、节点数、分片数等信息
"""
with get_client() as client:
r = client.get("/_cluster/health")
return r.json()
@mcp.tool()
def cluster_stats() -> dict:
"""
获取集群统计信息
包括文档数、存储大小、索引数量等
"""
with get_client() as client:
r = client.get("/_cluster/stats")
data = r.json()
# 精简返回,避免太长
return {
"cluster_name": data.get("cluster_name"),
"status": data.get("status"),
"nodes": data.get("nodes", {}).get("count", {}),
"indices": {
"count": data.get("indices", {}).get("count"),
"docs": data.get("indices", {}).get("docs", {}),
"store_size": data.get("indices", {}).get("store", {}).get("size_in_bytes")
}
}
# ============ 索引操作 ============
@mcp.tool()
def list_indices() -> list:
"""
列出所有索引
返回索引名称、文档数、存储大小、健康状态
"""
with get_client() as client:
r = client.get("/_cat/indices?format=json")
indices = r.json()
return [{
"index": idx.get("index"),
"health": idx.get("health"),
"status": idx.get("status"),
"docs_count": idx.get("docs.count"),
"store_size": idx.get("store.size")
} for idx in indices if not idx.get("index", "").startswith(".")]
@mcp.tool()
def get_index_mapping(index: str) -> dict:
"""
获取索引的字段映射(schema)
参数:
index: 索引名称
"""
with get_client() as client:
r = client.get(f"/{index}/_mapping")
return r.json()
@mcp.tool()
def create_index(index: str, mappings: dict = None, settings: dict = None) -> dict:
"""
创建新索引
参数:
index: 索引名称
mappings: 字段映射定义(可选)
settings: 索引设置如分片数(可选)
示例 mappings:
{"properties": {"title": {"type": "text"}, "count": {"type": "integer"}}}
"""
body = {}
if mappings:
body["mappings"] = mappings
if settings:
body["settings"] = settings
with get_client() as client:
r = client.put(f"/{index}", json=body if body else None)
return r.json()
@mcp.tool()
def delete_index(index: str) -> dict:
"""
删除索引(危险操作,会删除所有数据)
参数:
index: 要删除的索引名称
"""
with get_client() as client:
r = client.delete(f"/{index}")
return r.json()
# ============ 文档操作 ============
@mcp.tool()
def index_document(index: str, document: dict, doc_id: str = None) -> dict:
"""
写入单个文档
参数:
index: 索引名称
document: 文档内容(JSON 对象)
doc_id: 文档 ID(可选,不传则自动生成)
示例:
index_document("products", {"name": "iPhone", "price": 999})
"""
with get_client() as client:
if doc_id:
r = client.put(f"/{index}/_doc/{doc_id}", json=document)
else:
r = client.post(f"/{index}/_doc", json=document)
return r.json()
@mcp.tool()
def get_document(index: str, doc_id: str) -> dict:
"""
根据 ID 获取文档
参数:
index: 索引名称
doc_id: 文档 ID
"""
with get_client() as client:
r = client.get(f"/{index}/_doc/{doc_id}")
return r.json()
@mcp.tool()
def delete_document(index: str, doc_id: str) -> dict:
"""
删除单个文档
参数:
index: 索引名称
doc_id: 文档 ID
"""
with get_client() as client:
r = client.delete(f"/{index}/_doc/{doc_id}")
return r.json()
@mcp.tool()
def bulk_index(index: str, documents: list) -> dict:
"""
批量写入文档
参数:
index: 索引名称
documents: 文档列表
示例:
bulk_index("products", [{"name": "A"}, {"name": "B"}])
"""
# 构建 bulk 请求体
lines = []
for doc in documents:
lines.append(json.dumps({"index": {"_index": index}}))
lines.append(json.dumps(doc))
body = "\n".join(lines) + "\n"
with get_client() as client:
r = client.post(
"/_bulk",
content=body,
headers={"Content-Type": "application/x-ndjson"}
)
result = r.json()
return {
"took": result.get("took"),
"errors": result.get("errors"),
"items_count": len(result.get("items", []))
}
# ============ 搜索 ============
@mcp.tool()
def search(index: str, query: dict, size: int = 10) -> dict:
"""
执行搜索查询
参数:
index: 索引名称(可用 * 搜索所有索引)
query: Elasticsearch DSL 查询
size: 返回结果数量,默认 10
示例 - 全文搜索:
search("products", {"match": {"name": "iPhone"}})
示例 - 精确匹配:
search("products", {"term": {"status": "active"}})
示例 - 范围查询:
search("products", {"range": {"price": {"gte": 100, "lte": 500}}})
"""
body = {
"query": query,
"size": size
}
with get_client() as client:
r = client.post(f"/{index}/_search", json=body)
result = r.json()
hits = result.get("hits", {})
return {
"total": hits.get("total", {}).get("value", 0),
"took_ms": result.get("took"),
"hits": [{
"_id": h.get("_id"),
"_score": h.get("_score"),
"_source": h.get("_source")
} for h in hits.get("hits", [])]
}
@mcp.tool()
def search_simple(index: str, keyword: str, field: str = "_all", size: int = 10) -> dict:
"""
简单关键词搜索(适合不熟悉 DSL 的场景)
参数:
index: 索引名称
keyword: 搜索关键词
field: 搜索字段,默认全字段
size: 返回数量
示例:
search_simple("products", "iPhone")
search_simple("logs", "error", field="message")
"""
if field == "_all":
query = {"query_string": {"query": keyword}}
else:
query = {"match": {field: keyword}}
return search(index, query, size)
@mcp.tool()
def aggregate(index: str, field: str, agg_type: str = "terms", size: int = 10) -> dict:
"""
聚合统计
参数:
index: 索引名称
field: 聚合字段
agg_type: 聚合类型 - terms(分组计数), avg, sum, min, max, cardinality(去重计数)
size: 返回桶数量(仅 terms 有效)
示例:
aggregate("orders", "status", "terms") # 按状态分组计数
aggregate("orders", "amount", "avg") # 计算平均金额
"""
if agg_type == "terms":
agg_body = {"terms": {"field": field, "size": size}}
else:
agg_body = {agg_type: {"field": field}}
body = {
"size": 0,
"aggs": {"result": agg_body}
}
with get_client() as client:
r = client.post(f"/{index}/_search", json=body)
result = r.json()
agg_result = result.get("aggregations", {}).get("result", {})
if agg_type == "terms":
return {
"buckets": [{
"key": b.get("key"),
"count": b.get("doc_count")
} for b in agg_result.get("buckets", [])]
}
else:
return {"value": agg_result.get("value")}
# ============ 运行 ============
if __name__ == "__main__":
mcp.run()
---
## 被天才吧耽误,Apple silicon 死机与恢复的一年
- URL: https://blog.no-claw.com/posts/d5918d57/
- 日期: 2025-12-21
- 分类: 电子产品 / 电脑 / 苹果
- 标签: Apple
十年的黑苹果玩家终于入手了Apple silicon,且不说买了半年之后就被新产品的pro背刺的感受。这次我们只聊由于性能问题被耽误的这一年。
新电子产品的第一年都很稳,各种满核心编译Java都没有问题。不知道是系统更新还是厂家为了迭代产品,基本都是快过保的时候出问题。
去年冬天突然遇到死机黑屏问题,Typora文档写一半黑屏了,长按电源键无法开机,来回折腾要半个小时以上。去天才吧维修,说硬件测试没问题(其实就是快捷键), 千篇一律的官僚主义(记住这个词后面要考)拒绝返厂,然后拒绝我的各种专业建议,给我的建议就是备份重装,但是,崩溃依然继续。 于是求助群友,我竟然不是个例。
半年后,继续找他们掰扯,理由是过了一段时间无法证明我说的重装系统不能复现问题。给的方案还是重装。继续拒绝返厂检测。
期间也陆陆续续和线上沟通,在这期间对这个品牌的售后彻底失望。行政关系团队的陈先生,带着一种高高在上的口吻来“解决问题”。关于我反馈他们的专员一问三不知,完全有失一个技术人应有的技术储备的时候。他轻描淡写谈写,“我不是技术方向的,所以我不对技术层面的事情做评价,我们的专员是经过专业培训的,所以你要相信他们”。然后他们向上反馈,客户不配合,无法继续排查问题,服务被迫终止。留下一句,你要去网上发帖子是你的权利云云。仿佛这一切就是他们的日常。
收集了日志,和我说是电脑plist文件格式有问题,于是反问哪个软件的?这个你们xcode为什么不校验?对方摆烂。让我做删除某某目录的操作,问这个操作是做什么的,删除有什么后果,对方语塞。还好有备份,这个操作会直接导致系统里的ruby无法使用,而MacOS的一些组件还是依赖这个的。
不管是电话里还是天才吧,要录音留影的时候,对方理直气壮的拒绝服务,是真的不敢么。
所以啊,当致电400的时候,apple电话里的“请礼貌对待他们”,有没有可能真的是你们的问题呢?
如果国产OS做的好一点,不那么好大喜功,可能国产真的很符合国人的使用习惯,赶超欧美也只是时间的问题。
于是心得:如果去找Apple线上的客服问问题,可能还没有自己查文档来的快。
这已经不是,某天才吧店,听着所谓的专员对着客户说,“IOS之所以能成为一个独特的操作系统,原因….” 小伙伴说他是伪科普,我说现在已经不是10年前了。所谓apple专员的话术在当年可能觉得专业,现在只是一种甩锅的手段罢了。
言归正传,说到Macbook的崩溃,随着时间关系,已经从写写文档直接黑屏无法启动变成了睡眠直接睡死,然后变成了做一些文字工作鼠标卡死,SWAP爆满。虽然我的使用习惯不是特别好,但是这些年的黑苹果,从来没出现过这些问题,所以对Macbook 抱有希望,全家桶是果黑的第一步,没错。苹果生态很好,但不是唯一。
一次偶然的机会,了解到kernel task。默认的任务管理器是不显示的,也就是我们看系统监控的时候,看到的占用总是和实际不符。
1
活动监视器 - 显示 - 所有进程,分层显示。
关于kernel task,有说左边插显示器会导致温度异常的,有说系统过热保护的。但,只有跑大模型的时候风扇才会拉起来,但是这不等于机身不烫。通常来说,电脑卡的时候,kernel task的CPU占用率通常拉到100%,极端的时候我的CPU可以拉到接近500。
说句玩笑话,因为系统的自我保护,包括但不限于温度过高,负载过重等等,这个保护进程会占用大量的CPU和内存资源。从而导致系统卡顿,打字延迟,开会音频撕裂。在那些年的黑苹果生涯,我从未遇到过。
kernel task这个进程,不能被kill。所以最后的方案就是重启电脑,以及在下次打开的时候要至少停止30秒,或者什么也不干,让CPU当下此刻满载让kernel task充分发挥威力,一直到CPU降下来。
也许我并不怀念那个需要熬夜改驱动的时代,我怀念的是那个技术至上、凡事求真求实的江湖。
当一家顶尖科技公司的售后开始用“我不懂技术”当挡箭牌,用“相信培训”来掩盖逻辑缺失时,对于拥有专业技能的发烧友来讲,这是不止是傲慢,更是对技术本身的亵渎与放逐。
折腾了十年,我躲过了黑苹果遍地的驱动坑,却终究没躲过白苹果这堵官僚墙。
---
## Amazon Bedrock AgentCore 开发实战(一):本地构建 AI Agent
- URL: https://blog.no-claw.com/posts/56feb9d1/
- 日期: 2025-12-14
- 分类: 软件 / AWS
- 标签: AWS
在 AI Agent 开发过程中,快速迭代和即时反馈至关重要。Amazon Bedrock AgentCore 提供了完整的本地开发支持,让开发者能够在本地环境中构建、测试和调试 Agent,然后无缝部署到云端。
本文将带您从零开始,在本地搭建 AgentCore 开发环境,并通过详细的代码解析,帮助您深入理解每一个技术细节。
Amazon Bedrock AgentCore 简介
Amazon Bedrock AgentCore 是一套专为 AI Agent 设计的企业级基础设施服务。它解决了 Agent 从原型到生产过程中的核心挑战:
AgentCore Runtime:无服务器运行时环境,支持最长 8 小时的任务执行和 100MB 的请求负载
AgentCore Memory:提供短期和长期记忆管理,支持跨会话的上下文保持
AgentCore Gateway:统一的工具网关,支持 MCP 协议的工具发现和调用
AgentCore Identity:安全的身份认证和授权管理
本地开发的优势在于:开发者可以在熟悉的环境中快速验证想法,无需等待云端部署,同时保持与生产环境一致的代码结构。
第一部分:环境准备
1.1 安装 Python 包管理器
我们推荐使用 uv 作为 Python 包管理器,它具有更快的依赖解析速度和更可靠的环境隔离能力。
macOS 和 Linux 系统:
1
curl -LsSf https://astral.sh/uv/install.sh | sh
Windows 系统:
1
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
安装完成后,请重新打开终端以使环境变量生效。
1.2 配置 AWS 凭证
AgentCore 需要访问 Amazon Bedrock 服务,因此需要配置有效的 AWS 凭证。
1
2
# 验证当前凭证配置
aws sts get-caller-identity
如果尚未配置凭证,请执行:
1
aws configure
系统会提示您输入 Access Key ID、Secret Access Key、默认区域和输出格式。
1.3 开启模型访问权限
在 AWS 控制台中,导航至 Amazon Bedrock 服务,在”Model access”页面中启用 Anthropic Claude 系列模型的访问权限。本文示例使用 Claude Sonnet 4 模型。
1.4 创建项目并安装依赖
1
2
3
4
5
6
7
8
9
10
11
# 创建新项目,指定 Python 版本为 3.13
uv init my-agent-project --python 3.13
# 进入项目目录
cd my-agent-project
# 安装核心依赖包
uv add bedrock-agentcore strands-agents
# 安装开发工具包(用于后续部署)
uv add --dev bedrock-agentcore-starter-toolkit
依赖包说明:
bedrock-agentcore:AgentCore 的 Python SDK,提供运行时封装和服务集成
strands-agents:Strands Agent 框架,简化 Agent 的构建过程
bedrock-agentcore-starter-toolkit:部署工具包,提供 CLI 命令和自动化部署能力
第二部分:构建第一个本地 Agent
2.1 创建 Agent 入口文件
在项目根目录下创建 main.py 文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from bedrock_agentcore import BedrockAgentCoreApp
from strands import Agent
from strands.models import BedrockModel
app = BedrockAgentCoreApp(debug=True)
model = BedrockModel(
model_id="us.anthropic.claude-sonnet-4-20250514-v1:0",
temperature=0.7,
max_tokens=4096
)
agent = Agent(
model=model,
system_prompt="你是一个专业的技术助手,回答问题时准确、简洁。"
)
@app.entrypoint
def invoke(payload):
user_message = payload.get("prompt", "")
if not user_message:
return {"error": "prompt 参数不能为空"}
result = agent(user_message)
return {"response": result.message["content"][0]["text"]}
if __name__ == "__main__":
app.run()
2.2 代码逐行解析
让我们详细分析每一部分代码的作用:
导入模块:
1
from bedrock_agentcore import BedrockAgentCoreApp
BedrockAgentCoreApp 是 AgentCore 应用的核心类。它封装了 HTTP 服务器、请求路由和生命周期管理,使您的 Agent 代码能够以标准化的方式运行,无论是在本地还是云端。
1
from strands import Agent
Agent 是 Strands 框架的核心类,负责管理对话流程、工具调用和模型交互。它提供了简洁的 API 来构建具有推理能力的 AI Agent。
1
from strands.models import BedrockModel
BedrockModel 是 Bedrock 模型的封装类,处理与 Amazon Bedrock 服务的通信,包括认证、请求格式化和响应解析。
创建应用实例:
1
app = BedrockAgentCoreApp(debug=True)
创建 AgentCore 应用实例。debug=True 参数启用调试模式,会输出详细的请求和响应日志,便于开发阶段排查问题。在生产环境中,建议将此参数设为 False。
配置模型:
1
2
3
4
5
model = BedrockModel(
model_id="us.anthropic.claude-sonnet-4-20250514-v1:0",
temperature=0.7,
max_tokens=4096
)
model_id:指定使用的基础模型。这里使用 Claude Sonnet 4,us. 前缀表示使用美国区域的跨区域推理端点
temperature:控制输出的随机性,范围 0-1。较低的值产生更确定性的输出,较高的值增加创造性
max_tokens:限制单次响应的最大 token 数量,防止输出过长
创建 Agent:
1
2
3
4
agent = Agent(
model=model,
system_prompt="你是一个专业的技术助手,回答问题时准确、简洁。"
)
model:传入上面配置的模型实例
system_prompt:系统提示词,定义 Agent 的角色和行为准则。这段文本会在每次对话开始时发送给模型,引导其行为
定义入口函数:
1
2
@app.entrypoint
def invoke(payload):
@app.entrypoint 装饰器将 invoke 函数注册为 Agent 的入口点。当收到 HTTP 请求时,AgentCore 会自动调用此函数,并将请求体解析为 payload 字典传入。
1
user_message = payload.get("prompt", "")
从请求负载中提取 prompt 字段。使用 get 方法并提供默认值,可以安全地处理字段缺失的情况。
1
2
if not user_message:
return {"error": "prompt 参数不能为空"}
输入验证:确保用户提供了有效的输入。返回的字典会被自动序列化为 JSON 响应。
1
result = agent(user_message)
调用 Agent 处理用户消息。Agent 会将消息发送给模型,获取响应,并在需要时执行工具调用。返回的 result 对象包含完整的对话结果。
1
return {"response": result.message["content"][0]["text"]}
从结果中提取文本响应并返回。result.message 是模型返回的消息对象,content 是内容数组,我们取第一个元素的 text 字段。
启动应用:
1
2
if __name__ == "__main__":
app.run()
Python 的标准入口点检查。当直接运行此文件时,启动 HTTP 服务器。app.run() 默认在 0.0.0.0:8080 启动服务,这是 AgentCore Runtime 的标准端口。
2.3 运行和测试
启动 Agent:
打开终端,在项目目录下执行:
1
uv run main.py
您将看到类似以下的输出:
1
2
3
4
INFO: Started server process [12345]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8080
发送测试请求:
打开另一个终端窗口,使用 curl 发送请求:
1
2
3
curl -X POST http://localhost:8080/invocations \
-H "Content-Type: application/json" \
-d '{"prompt": "请解释什么是微服务架构"}' | jq .
运行结果示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
"response": "微服务架构是一种软件设计模式,它将大型应用程序拆分为多个小型、独立的服务,每个服务负责特定的业务功能。
### 核心特征
**1. 服务独立性**
- 每个微服务可以独立开发、部署和扩展
- 拥有自己的数据库和业务逻辑
- 通过 API 进行通信
**2. 单一职责**
- 每个服务专注于一个业务领域
- 服务边界清晰,功能内聚
**3. 去中心化**
- 没有中央控制器
- 服务间通过轻量级协议通信(如 HTTP/REST、消息队列)
### 主要优势
- **可扩展性**:可针对性地扩展高负载服务
- **技术多样性**:不同服务可使用不同技术栈
- **故障隔离**:单个服务故障不会影响整个系统
- **开发效率**:小团队可独立维护服务
- **部署灵活**:支持持续集成和部署
### 主要挑战
- **复杂性增加**:网络通信、数据一致性问题
- **运维成本**:需要更多的监控和管理工具
- **性能开销**:服务间调用的网络延迟
- **分布式系统问题**:如分布式事务、服务发现
微服务架构特别适合大型、复杂的企业应用,以及需要快速迭代和高可用性的系统。"
}
从响应结果可以看到,Agent 成功接收了请求并返回了结构化的回答。响应时间约为 9 秒,这包括了模型推理的时间。
请求解析:
-X POST:指定 HTTP 方法为 POST
http://localhost:8080/invocations:AgentCore 的标准调用端点
-H "Content-Type: application/json":设置请求头,表明请求体为 JSON 格式
-d '{"prompt": "..."}':请求体,包含发送给 Agent 的消息
| jq .:将响应通过 jq 工具格式化输出(需要预先安装 jq)
第三部分:为 Agent 添加工具能力
Agent 的真正威力在于能够调用外部工具来完成任务。接下来,我们将为 Agent 添加自定义工具。
3.1 安装工具依赖
1
uv add strands-agents-tools
strands-agents-tools 包含了一系列预构建的工具,如计算器、文件操作等。
3.2 创建带工具的 Agent
更新 main.py 文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from bedrock_agentcore import BedrockAgentCoreApp
from strands import Agent, tool
from strands.models import BedrockModel
from strands_tools import calculator
app = BedrockAgentCoreApp(debug=True)
@tool
def get_weather(city: str) -> str:
"""
获取指定城市的当前天气信息。
Args:
city: 城市名称,例如 "北京"、"上海"、"深圳"
Returns:
包含温度和天气状况的字符串
"""
weather_data = {
"北京": "晴天,气温 22°C,湿度 45%",
"上海": "多云,气温 26°C,湿度 65%",
"深圳": "阵雨,气温 28°C,湿度 80%",
}
return weather_data.get(city, f"暂无 {city} 的天气数据")
@tool
def search_database(query: str, limit: int = 5) -> str:
"""
在数据库中搜索相关信息。
Args:
query: 搜索关键词
limit: 返回结果的最大数量,默认为 5
Returns:
搜索结果的摘要信息
"""
return f"找到 {limit} 条与「{query}」相关的记录(模拟数据)"
model = BedrockModel(
model_id="us.anthropic.claude-sonnet-4-20250514-v1:0"
)
agent = Agent(
model=model,
tools=[calculator, get_weather, search_database],
system_prompt="""你是一个智能助手,具备以下能力:
1. 使用 calculator 工具进行数学计算
2. 使用 get_weather 工具查询城市天气
3. 使用 search_database 工具搜索数据库
请根据用户的问题,选择合适的工具来提供准确的答案。"""
)
@app.entrypoint
def invoke(payload):
user_message = payload.get("prompt", "")
if not user_message:
return {"error": "prompt 参数不能为空"}
result = agent(user_message)
return {"response": result.message["content"][0]["text"]}
if __name__ == "__main__":
app.run()
3.3 工具定义详解
@tool 装饰器:
1
2
@tool
def get_weather(city: str) -> str:
@tool 装饰器将普通 Python 函数转换为 Agent 可调用的工具。Strands 框架会自动:
提取函数签名作为工具的输入参数定义
解析 docstring 作为工具的描述信息
处理参数类型转换和验证
类型注解的重要性:
1
def get_weather(city: str) -> str:
city: str:参数类型注解,告诉模型这个参数应该是字符串类型
-> str:返回值类型注解,表明函数返回字符串
类型注解不仅是代码文档,更是模型理解工具用法的关键信息。模型会根据这些信息决定如何构造工具调用参数。
Docstring 规范:
1
2
3
4
5
6
7
8
9
"""
获取指定城市的当前天气信息。
Args:
city: 城市名称,例如 "北京"、"上海"、"深圳"
Returns:
包含温度和天气状况的字符串
"""
Docstring 遵循 Google 风格,包含三个部分:
第一行:工具的简要描述,模型会用它来判断何时使用此工具
Args 部分:每个参数的详细说明,包括示例值
Returns 部分:返回值的描述
清晰的 docstring 能显著提高模型选择和使用工具的准确性。
带默认值的参数:
1
def search_database(query: str, limit: int = 5) -> str:
limit: int = 5 定义了一个带默认值的可选参数。模型可以选择是否提供此参数,如果不提供则使用默认值 5。
注册工具到 Agent:
1
2
3
4
5
agent = Agent(
model=model,
tools=[calculator, get_weather, search_database],
...
)
tools 参数接受一个工具列表。calculator 是从 strands_tools 导入的内置工具,get_weather 和 search_database 是我们自定义的工具。
3.4 测试工具调用
重启 Agent 后,测试不同类型的请求:
测试计算功能:
1
2
3
curl -X POST http://localhost:8080/invocations \
-H "Content-Type: application/json" \
-d '{"prompt": "计算 (125 + 375) * 2.5 的结果"}' | jq .
Agent 会识别这是一个数学计算问题,自动调用 calculator 工具。
计算功能运行结果:
1
2
3
4
5
6
7
{
"response": "计算结果:(125 + 375) * 2.5 = 1250
计算过程:
1. 先计算括号内:125 + 375 = 500
2. 再乘以 2.5:500 * 2.5 = 1250"
}
测试天气查询:
1
2
3
curl -X POST http://localhost:8080/invocations \
-H "Content-Type: application/json" \
-d '{"prompt": "上海今天天气怎么样?"}' | jq .
Agent 会调用 get_weather 工具,传入参数 city="上海"。
天气查询运行结果:
1
2
3
4
5
{
"response": "上海今天的天气情况:多云,气温 26°C,湿度 65%。
温度适中,湿度稍高,建议外出时携带一把伞以防万一。"
}
测试复合任务:
1
2
3
curl -X POST http://localhost:8080/invocations \
-H "Content-Type: application/json" \
-d '{"prompt": "查一下北京的天气,如果温度超过20度,计算开10小时空调需要多少度电(假设功率1.5千瓦)"}' | jq .
这个请求需要 Agent 协调多个工具:先查询天气获取温度,然后根据条件决定是否进行计算。
复合任务运行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"response": "让我先查询北京的天气,然后根据温度情况进行计算。
**北京天气**:晴天,气温 22°C,湿度 45%
由于当前温度 22°C 超过了 20 度,我来计算空调用电量:
**计算过程**:
- 空调功率:1.5 千瓦(kW)
- 使用时间:10 小时
- 耗电量 = 功率 × 时间 = 1.5 kW × 10 h = 15 度电
**结论**:开 10 小时空调大约需要消耗 15 度电。按照一般民用电价(约 0.5 元/度)计算,费用约为 7.5 元。"
}
从这个结果可以看到,Agent 成功地:
调用 get_weather 工具获取北京天气
判断温度(22°C)超过 20 度
调用 calculator 工具计算用电量
综合信息给出完整回答
第四部分:构建 MCP Server
Model Context Protocol(MCP)是一种标准化协议,允许 Agent 动态发现和调用工具。MCP Server 可以被任何支持 MCP 的客户端(如 Claude Desktop、Cursor、Kiro 等)调用。
4.1 安装 MCP 依赖
1
uv add mcp
4.2 创建 MCP Server
创建新文件 mcp_server.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(
name="Financial Tools Server",
host="0.0.0.0",
stateless_http=True
)
@mcp.tool()
def add_numbers(a: int, b: int) -> int:
"""Add two numbers together"""
return a + b
@mcp.tool()
def multiply_numbers(a: int, b: int) -> int:
"""Multiply two numbers together"""
return a * b
@mcp.tool()
def get_stock_price(symbol: str) -> str:
"""
Get the current stock price for a given symbol.
Args:
symbol: Stock ticker symbol like AAPL, GOOGL, AMZN
Returns:
Stock symbol and current price
"""
prices = {
"AAPL": 178.50,
"GOOGL": 141.20,
"AMZN": 178.90,
"MSFT": 378.50
}
price = prices.get(symbol.upper())
if price:
return f"{symbol.upper()} current price: ${price}"
return f"Price data not found for {symbol}"
@mcp.tool()
def greet_user(name: str) -> str:
"""Greet a user by name"""
return f"Hello, {name}! Nice to meet you."
if __name__ == "__main__":
print("MCP Server 启动中...")
print("MCP 端点: http://localhost:8000/mcp")
mcp.run(transport="streamable-http")
4.3 MCP Server 代码解析
导入 FastMCP:
1
from mcp.server.fastmcp import FastMCP
FastMCP 是 MCP Python SDK 提供的高级封装类,简化了 MCP Server 的创建过程。
创建 MCP Server 实例:
1
2
3
4
5
mcp = FastMCP(
name="Financial Tools Server",
host="0.0.0.0",
stateless_http=True
)
name:Server 的名称,用于标识和日志记录
host:监听地址,0.0.0.0 表示接受所有网络接口的连接
stateless_http=True:启用无状态 HTTP 模式,这是 AgentCore Runtime 要求的配置
定义工具:
1
2
3
4
@mcp.tool()
def add_numbers(a: int, b: int) -> int:
"""Add two numbers together"""
return a + b
@mcp.tool() 装饰器将 Python 函数注册为 MCP 工具:
函数名成为工具名称
参数类型注解定义输入参数的类型
docstring 成为工具的描述信息
返回值类型注解定义输出类型
启动服务:
1
mcp.run(transport="streamable-http")
transport="streamable-http" 指定使用 Streamable HTTP 传输协议,这是 MCP 推荐的生产环境传输方式,支持流式响应。
4.4 运行和测试 MCP Server
启动服务:
1
uv run mcp_server.py
运行输出:
1
2
3
4
MCP Server 启动中...
MCP 端点: http://localhost:8000/mcp
INFO: Started server process [12345]
INFO: Uvicorn running on http://0.0.0.0:8000
使用 Python 客户端测试:
首先安装 MCP 客户端依赖(如果尚未安装):
1
uv add mcp httpx
创建测试客户端文件 mcp_client.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async def main():
"""测试 MCP Server 的连接和工具调用"""
mcp_url = "http://localhost:8000/mcp"
print(f"连接到 MCP Server: {mcp_url}")
# 建立连接
async with streamablehttp_client(mcp_url) as (
read_stream,
write_stream,
_,
):
async with ClientSession(read_stream, write_stream) as session:
# 初始化会话
await session.initialize()
print("✓ 连接成功\n")
# 列出可用工具
tools = await session.list_tools()
print("可用工具:")
for tool in tools.tools:
print(f" - {tool.name}: {tool.description}")
print("\n" + "=" * 50 + "\n")
# 测试 add_numbers 工具
result = await session.call_tool("add_numbers", {"a": 10, "b": 20})
print(f"add_numbers(10, 20) = {result.content[0].text}")
# 测试 multiply_numbers 工具
result = await session.call_tool("multiply_numbers", {"a": 7, "b": 8})
print(f"multiply_numbers(7, 8) = {result.content[0].text}")
# 测试 get_stock_price 工具
result = await session.call_tool("get_stock_price", {"symbol": "AAPL"})
print(f"get_stock_price('AAPL') = {result.content[0].text}")
# 测试 greet_user 工具
result = await session.call_tool("greet_user", {"name": "开发者"})
print(f"greet_user('开发者') = {result.content[0].text}")
if __name__ == "__main__":
asyncio.run(main())
代码解析:
1
2
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
导入 MCP 客户端所需的模块:
ClientSession:MCP 客户端会话管理类
streamablehttp_client:Streamable HTTP 传输客户端,用于连接使用该协议的 MCP Server
1
async with streamablehttp_client(mcp_url) as (read_stream, write_stream, _):
建立与 MCP Server 的连接,返回读写流用于双向通信。
1
2
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
创建客户端会话并初始化,这会与服务器交换能力信息。
1
tools = await session.list_tools()
获取服务器提供的所有工具列表。
1
result = await session.call_tool("add_numbers", {"a": 10, "b": 20})
调用指定工具,传入参数字典,返回执行结果。
运行测试:
确保 MCP Server 正在运行,然后执行:
1
uv run mcp_client.py
运行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
连接到 MCP Server: http://localhost:8000/mcp
✓ 连接成功
可用工具:
- add_numbers: Add two numbers together
- multiply_numbers: Multiply two numbers together
- get_stock_price: Get the current stock price for a given symbol.
- greet_user: Greet a user by name
==================================================
add_numbers(10, 20) = 30
multiply_numbers(7, 8) = 56
get_stock_price('AAPL') = AAPL current price: $178.5
greet_user('开发者') = Hello, 开发者! Nice to meet you.
使用 curl 测试(CLI 方式):
MCP Server 使用 Streamable HTTP 传输协议,需要设置正确的 Accept header:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 初始化连接
curl -X POST http://localhost:8000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "curl-client", "version": "1.0.0"}
}
}'
1
2
3
4
5
# 列出可用工具
curl -X POST http://localhost:8000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}'
1
2
3
4
5
6
7
8
9
10
# 调用 add_numbers 工具
curl -X POST http://localhost:8000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {"name": "add_numbers", "arguments": {"a": 10, "b": 20}}
}'
curl 响应示例:
列出工具的响应:
1
2
3
4
5
6
7
8
9
10
11
12
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{"name": "add_numbers", "description": "Add two numbers together", ...},
{"name": "multiply_numbers", "description": "Multiply two numbers together", ...},
{"name": "get_stock_price", "description": "Get the current stock price for a given symbol.", ...},
{"name": "greet_user", "description": "Greet a user by name", ...}
]
}
}
调用工具的响应:
1
{"jsonrpc": "2.0", "id": 3, "result": {"content": [{"type": "text", "text": "30"}]}}
注意:curl 命令必须包含 -H "Accept: application/json, text/event-stream" header,否则会收到 “Not Acceptable” 错误。
使用 MCP Inspector 测试(可选):
MCP Inspector 是一个可视化测试工具,提供图形界面来测试 MCP Server:
1
npx @modelcontextprotocol/inspector
在浏览器中打开 http://localhost:6274,选择 “Streamable HTTP” 传输类型,输入 MCP Server 地址 http://localhost:8000/mcp,即可进行交互式测试。您可以:
查看所有可用工具及其参数定义
手动调用工具并查看返回结果
调试工具执行过程
第五部分:构建 A2A Server
Agent-to-Agent(A2A)协议专为多 Agent 系统设计,允许 Agent 之间相互发现和通信。A2A 使用 JSON-RPC 格式进行通信,并通过 Agent Card 描述 Agent 的能力。
5.1 安装 A2A 依赖
1
uv add 'strands-agents[a2a]'
5.2 创建 A2A Server
创建新文件 a2a_server.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import os
import logging
from strands import Agent
from strands.multiagent.a2a import A2AServer
from strands.models import BedrockModel
from strands_tools import calculator
import uvicorn
from fastapi import FastAPI
logging.basicConfig(level=logging.INFO)
# 从环境变量获取运行时 URL,本地开发时使用默认值
runtime_url = os.environ.get(
'AGENTCORE_RUNTIME_URL',
'http://127.0.0.1:9000/'
)
logging.info(f"Runtime URL: {runtime_url}")
# 创建模型
model = BedrockModel(
model_id="us.anthropic.claude-sonnet-4-20250514-v1:0"
)
# 创建 Agent
strands_agent = Agent(
name="Calculator Agent",
description="A calculator agent that can perform basic arithmetic operations.",
model=model,
tools=[calculator],
callback_handler=None
)
# 创建 A2A Server
a2a_server = A2AServer(
agent=strands_agent,
http_url=runtime_url,
serve_at_root=True # 在根路径提供服务
)
# 创建 FastAPI 应用
app = FastAPI(title="Calculator A2A Server")
@app.get("/ping")
def health_check():
"""健康检查端点"""
return {"status": "healthy"}
# 挂载 A2A Server
app.mount("/", a2a_server.to_fastapi_app())
if __name__ == "__main__":
print("A2A Server 启动中...")
print("Agent Card: http://localhost:9000/.well-known/agent-card.json")
print("健康检查: http://localhost:9000/ping")
uvicorn.run(app, host="0.0.0.0", port=9000)
5.3 A2A Server 代码解析
导入 A2A 模块:
1
from strands.multiagent.a2a import A2AServer
A2AServer 是 Strands SDK 提供的 A2A 协议实现,需要安装 strands-agents[a2a] 扩展包。
环境变量配置:
1
2
3
4
runtime_url = os.environ.get(
'AGENTCORE_RUNTIME_URL',
'http://127.0.0.1:9000/'
)
从环境变量读取运行时 URL,如果未设置则使用本地默认值。这种设计使同一份代码可以在本地和云端运行,只需通过环境变量切换配置。部署到 AgentCore Runtime 时,该环境变量会自动设置。
创建 A2A Server:
1
2
3
4
5
a2a_server = A2AServer(
agent=strands_agent,
http_url=runtime_url,
serve_at_root=True
)
agent:要暴露的 Strands Agent 实例
http_url:Agent 的可访问 URL,用于生成 Agent Card 中的端点信息
serve_at_root=True:在根路径(/)提供服务,这是 AgentCore Runtime 要求的配置
组合 FastAPI 应用:
1
2
3
4
5
6
7
app = FastAPI(title="Calculator A2A Server")
@app.get("/ping")
def health_check():
return {"status": "healthy"}
app.mount("/", a2a_server.to_fastapi_app())
我们创建一个 FastAPI 应用作为容器,添加健康检查端点,然后将 A2A Server 挂载到根路径。A2A Server 默认使用 9000 端口,与 MCP Server(8000)和 AgentCore Runtime(8080)区分。
5.3 运行和测试 A2A Server
启动服务:
1
uv run a2a_server.py
获取 Agent Card:
1
curl http://localhost:9000/.well-known/agent-card.json | jq .
Agent Card 是 A2A 协议的核心概念,它描述了 Agent 的能力、支持的协议和调用方式。其他 Agent 可以通过读取 Agent Card 来了解如何与此 Agent 交互。
调用 Agent(使用 JSON-RPC 格式):
由于我们创建的是 Calculator Agent(计算器 Agent),测试请求应该是数学计算相关的问题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
curl -X POST http://localhost:9000/ \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "request-001",
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [
{
"kind": "text",
"text": "请计算 (256 + 128) * 3 的结果"
}
],
"messageId": "msg-001"
}
}
}' | jq .
请求格式解析:
jsonrpc:JSON-RPC 协议版本
id:请求标识符,用于匹配响应
method:调用的方法,message/send 是发送消息的标准方法
params.message:消息内容
role:消息角色,user 表示用户消息
parts:消息内容数组,支持多模态(文本、图片等)
messageId:消息唯一标识
运行结果示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"jsonrpc": "2.0",
"id": "request-001",
"result": {
"artifacts": [
{
"artifactId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "agent_response",
"parts": [
{
"kind": "text",
"text": "让我来计算这个表达式。\n\n**计算过程:**\n1. 先计算括号内:256 + 128 = 384\n2. 再乘以 3:384 × 3 = 1152\n\n**结果:(256 + 128) × 3 = 1152**"
}
]
}
],
"contextId": "65c4af3b-ef98-4c3a-9433-1287936d9703"
}
}
从响应可以看到,Calculator Agent 成功调用了 calculator 工具完成了数学计算,并返回了详细的计算过程。
第六部分:本地容器化部署
在部署到云端之前,我们可以先在本地使用 Docker 容器运行 Agent。AgentCore Starter Toolkit 提供了便捷的 CLI 命令,可以自动生成 Dockerfile 并在本地构建运行容器,无需手动编写配置文件。
7.1 配置 Agent
首先使用 agentcore configure 命令配置 Agent:
1
agentcore configure -e main.py
参数说明:
-e main.py:指定 Agent 的入口文件
如果是首次运行,CLI 会以交互式方式引导您完成配置,包括设置 Agent 名称、选择区域等。您也可以使用 -ni(non-interactive)参数跳过交互式提示,使用默认值:
1
agentcore configure -e main.py -ni
执行后,CLI 会在项目目录下生成 .agentcore/ 配置目录,包含配置文件和 Dockerfile。
7.2 本地容器运行
使用 agentcore launch --local 命令在本地 Docker 容器中构建并运行 Agent:
1
agentcore launch --local
此命令会自动:
生成优化的 Dockerfile
构建容器镜像
启动容器并映射端口 8080
挂载本地 AWS 凭证到容器中
输出容器日志到终端
运行输出示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
🏠 Launching Bedrock AgentCore (local mode)...
• Build and run container locally
• Requires Docker/Finch/Podman to be installed
• Perfect for development and testing
Launching Bedrock AgentCore agent 'main' locally
⠏ Launching Bedrock AgentCore...Docker image built: bedrock_agentcore-main:latest
✓ Docker image built: bedrock_agentcore-main:latest
✓ Ready to run locally
Starting server at http://localhost:8080
Press Ctrl+C to stop
Configuration of aws_configurator not loaded, configurator already loaded
Attempting to instrument while already instrumented
INFO: Started server process [1]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit)
按 Ctrl+C 可以停止容器。
注意:agentcore launch --local 需要本地安装 Docker、Finch 或 Podman。
7.3 测试容器化 Agent
在另一个终端窗口中,发送测试请求:
1
2
3
curl -X POST http://localhost:8080/invocations \
-H "Content-Type: application/json" \
-d '{"prompt": "用 Python 写一个快速排序算法"}' | jq .
运行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"response": "下面是 Python 实现的快速排序算法:
def quicksort(arr):
# 基准情况:空列表或单元素列表已经有序
if len(arr) <= 1:
return arr
# 选择中间元素作为基准值
pivot = arr[len(arr) // 2]
# 分区:小于、等于、大于基准值的元素
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
# 递归排序并合并结果
return quicksort(left) + middle + quicksort(right)
**算法说明:**
- 时间复杂度:平均 O(n log n),最坏 O(n²)
- 空间复杂度:O(n)(使用了额外的列表)"
}
7.4 本地容器运行的优势
使用 agentcore launch --local 相比直接 uv run main.py 有以下优势:
环境一致性:容器环境与云端 AgentCore Runtime 一致,避免”本地能跑,云端报错”的问题
依赖隔离:所有依赖都在容器内,不会污染本地 Python 环境
真实模拟:模拟生产环境的资源限制和网络配置
快速验证:在部署到云端前,先在本地验证容器化后的行为
7.5 查看生成的 Dockerfile
如果您想了解 CLI 生成的 Dockerfile 内容,可以查看:
1
cat .agentcore/Dockerfile
生成的 Dockerfile 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
FROM public.ecr.aws/docker/library/python:3.13-slim
WORKDIR /app
# 安装系统依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*
# 安装 uv 包管理器
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.local/bin:$PATH"
# 复制依赖文件并安装
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen
# 复制应用代码
COPY . .
# 暴露端口
EXPOSE 8080
# 启动命令
CMD ["uv", "run", "main.py"]
CLI 自动生成的 Dockerfile 遵循最佳实践,包括多阶段构建优化、依赖缓存等。
第七部分:部署到云端
本地开发和容器化测试完成后,可以使用 AgentCore Starter Toolkit 将 Agent 部署到 AWS。
7.1 使用 CLI 部署
如果您在第六部分已经执行过 agentcore configure,可以直接部署。否则先配置:
1
agentcore configure -e main.py
部署 Agent:
1
agentcore launch
此命令会(默认使用 CodeBuild 云端构建):
使用 AWS CodeBuild 构建 ARM64 容器镜像
将镜像推送到 ECR
创建 AgentCore Runtime
等待 Runtime 就绪
部署模式说明:
命令
构建位置
运行位置
说明
agentcore launch
云端 (CodeBuild)
云端 (AgentCore Runtime)
推荐,无需本地 Docker
agentcore launch --local
本地
本地
本地开发测试
agentcore launch --local-build
本地
云端
需要自定义构建时使用
测试云端 Agent:
1
agentcore invoke '{"prompt": "你好,这是来自云端的测试"}'
查看状态:
1
agentcore status
7.2 使用 Python SDK 部署
对于需要在 Jupyter Notebook 或 Python 脚本中进行更多控制的场景,可以使用 Python SDK:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from bedrock_agentcore_starter_toolkit import Runtime
from boto3.session import Session
# 获取当前区域
session = Session()
region = session.region_name
# 创建 Runtime 实例
runtime = Runtime()
# 配置部署参数
runtime.configure(
entrypoint="main.py",
auto_create_execution_role=True,
auto_create_ecr=True,
region=region,
agent_name="my_production_agent"
)
# 执行部署(默认使用 CodeBuild 云端构建)
runtime.launch()
print("部署成功!")
# 调用云端 Agent
response = runtime.invoke({
"prompt": "请介绍一下 Amazon Bedrock AgentCore"
})
print(f"响应: {response}")
# 清理资源请使用 CLI: agentcore destroy
配置输出示例:
1
2
3
4
Bedrock AgentCore configured: /path/to/project/.bedrock_agentcore.yaml
🚀 CodeBuild mode: building in cloud (RECOMMENDED - DEFAULT)
• Build ARM64 containers in the cloud with CodeBuild
• No local Docker required
部署模式说明:
Python SDK 支持与 CLI 相同的三种部署模式:
1
2
3
4
5
6
7
8
# 默认模式:CodeBuild 云端构建 + 云端部署(推荐)
runtime.launch()
# 本地开发模式:本地构建 + 本地运行
runtime.launch(local=True)
# 混合模式:本地构建 + 云端部署
runtime.launch(local_build=True)
7.3 清理资源
1
2
3
4
5
6
7
8
9
10
11
# 预览将要删除的资源(不实际删除)
agentcore destroy --dry-run
# 删除所有资源(保留 ECR 仓库,只删除镜像)
agentcore destroy
# 同时删除 ECR 仓库
agentcore destroy --delete-ecr-repo
# 跳过确认提示,直接删除
agentcore destroy --force
第八部分:常见问题与解决方案
8.1 端口占用
问题: 启动时提示端口 8080 已被占用
解决方案:
1
2
3
4
5
# 查找占用端口的进程
lsof -i :8080
# 终止进程(将 PID 替换为实际进程 ID)
kill -9
8.2 模型访问权限
问题: 调用时报 AccessDeniedException
解决方案:
确认 AWS 凭证有效:aws sts get-caller-identity
确认已在 Bedrock 控制台开启模型访问权限
确认使用的区域支持所选模型
8.3 工具未被调用
问题: Agent 没有使用定义的工具
解决方案:
检查 @tool 装饰器是否正确应用
确保 docstring 清晰描述了工具的用途
在 system_prompt 中明确说明可用的工具
检查工具是否已添加到 Agent 的 tools 列表
8.4 部署超时
问题: agentcore launch 执行时间过长
解决方案:
检查网络连接是否稳定
确认 IAM 权限包含 CodeBuild、ECR、Bedrock 相关权限
查看 CodeBuild 控制台中的构建日志
8.5 本地容器构建失败:Multiple top-level modules
问题: 执行 agentcore launch --local 时报错:
1
Multiple top-level modules discovered in a flat-layout: ['main', 'mcp_server', 'a2a_server', ...]
原因: 项目目录中有多个 Python 文件(如 main.py、mcp_server.py、a2a_server.py 等),setuptools 无法确定哪个是主模块。
解决方案:
在 pyproject.toml 中添加 [tool.setuptools] 配置,明确指定要打包的模块:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[project]
name = "my-agent-project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"bedrock-agentcore>=1.1.1",
"strands-agents>=1.19.0",
"strands-agents-tools>=0.2.17",
]
[dependency-groups]
dev = [
"bedrock-agentcore-starter-toolkit>=0.2.4",
]
[tool.setuptools]
py-modules = ["main"] # 只打包 main.py 作为主模块
添加 [tool.setuptools] 部分后,重新运行 agentcore launch --local 即可。
8.6 本地容器运行时 OTLP 导出错误
问题: 执行 agentcore launch --local 后,日志中出现大量重试警告:
1
2
Transient error StatusCode.UNAVAILABLE encountered while exporting traces to localhost:4317, retrying in 1s.
Transient error StatusCode.UNAVAILABLE encountered while exporting logs to localhost:4317, retrying in 2s.
原因: 容器内的 OpenTelemetry 尝试将追踪数据发送到 localhost:4317(默认 OTLP 端点),但本地没有运行 OTLP 接收服务。
解决方案:
这个警告不影响 Agent 正常运行,可以忽略。如果想消除警告,可以禁用 OTLP 导出:
1
2
3
agentcore launch --local \
--env OTEL_TRACES_EXPORTER=none \
--env OTEL_LOGS_EXPORTER=none
总结
本文详细介绍了 Amazon Bedrock AgentCore 的本地开发流程:
环境搭建:安装 uv、配置 AWS 凭证、创建项目
基础 Agent:使用 BedrockAgentCoreApp 和 Strands Agent 构建简单的对话 Agent
工具集成:通过 @tool 装饰器定义自定义工具,扩展 Agent 能力
MCP Server:构建 MCP 服务,支持工具发现和调用
A2A Server:构建支持 Agent 间通信的服务
本地容器化:使用 agentcore launch --local 在本地模拟生产环境
云端部署:使用 agentcore launch 一键部署到 AWS
通过本文的学习,您已经掌握了从本地开发到容器化测试,再到云端部署的完整流程。这种渐进式的开发方式能够帮助您:
在本地快速验证 Agent 逻辑
通过容器化确保环境一致性
无缝迁移到 AgentCore Runtime 生产环境
本地开发与云端部署的无缝衔接,使开发者能够在保持高效迭代的同时,确保代码在生产环境中的可靠运行。
相关资源
Amazon Bedrock AgentCore 官方文档:https://docs.aws.amazon.com/bedrock/latest/userguide/agentcore.html
Strands Agents SDK 文档:https://strandsagents.com/
MCP 协议规范:https://modelcontextprotocol.io/
A2A 协议规范:https://google.github.io/A2A/
AgentCore Starter Toolkit:https://github.com/aws/bedrock-agentcore-starter-toolkit
AgentCore Python SDK:https://github.com/aws/bedrock-agentcore-sdk-python
A2A 协议规范:https://a2a-protocol.org/
---
## 使用agentcore部署MCP server
- URL: https://blog.no-claw.com/posts/56feb9d1/
- 日期: 2025-12-14
- 分类: MCP / AWS
- 标签: AWS
agentcore除了可以部署agentcore应用程序之外,还可以部署MCP server。
换句话说,只要提供了容器镜像,ping和,那么就可以部署任意的应用程序,上面两个是比较典型的例子。
常规的MCP server 长这样,
1
如果要部署到云端的话,
1
默认会使用Agentcore runtime来启动,和sagemaker一样,需要有 /ping 和 /invokation的路由,并且容器host在8080端口,启动之后服务就会自动加一个网关来转发到这个应用的8080端口。
你可能还听见过Agentcore Gateway,这个是给一些其他的MCP server,Rest api或者。
甚至是apigateway和lambda。
启动之后,我们就会得到一个URL。
agentcore runtime的是
由于在url有arn,所以需要做一个http的转译,否则有些MCP的客户端可能无法处理识别这个。
agentcore gateway的是。
然后就是认证,Agentcore runtime接受JWT和IAM两种认证方式
而gateway除了这几种之外还可以接受无认证的方式。
这里我们重点介绍JWT的认证,一般是用于和SSO的集成,而这里的JWT通常指的是access token。
对于登陆系统而言,一开始使用用户名和密码登录之后,后端会返回一个JWT来代替用户的身份,然后我们把这个加在请求头里
请求头通常会这样写:
Authorization: Bearer
Authorization 是 HTTP 标头中的一个字段。
Bearer 表示这是一个 Bearer token 类型。
是实际的访问令牌。
例如,假设你获得了一个 access token,它可能长成这个样子:
Authorization: Bearer abcdefghijklmnopqrstuvwxyz1234567890
具体流程:
登录:用户通过用户名和密码登录后,身份提供者(如身份认证服务器)生成一个 access token,并将其返回给客户端(例如,浏览器、移动应用)。
访问资源:客户端在需要访问受保护资源时(例如,API),会在请求中添加 Authorization: Bearer 头部。这样,服务器就能知道这个请求背后是一个已经认证并授权的用户。
服务器验证:服务器接收到请求后,验证 Bearer token 是否有效。如果有效,服务器根据 token 中的权限信息返回相应的资源;如果无效或已过期,则拒绝访问。
---
## Milvus Workshop Web 版上线:4 万 Star 项目的官方实战教程,在手机上也能直接学了
- URL: https://blog.no-claw.com/posts/581baa02/
- 日期: 2025-12-11
- 分类: 软件
- 标签: 向量数据库
Milvus 作为全球最受欢迎的开源向量数据库,GitHub Star 数已突破 4 万。
向量数据库已经成为 AI 应用的核心基础设施。RAG 需要它存储知识库,Agent 需要它实现记忆,推荐系统需要它计算相似度,多模态搜索需要它做特征检索。从实验室到生产环境,向量检索已经是 AI 应用的标配能力。
但从入门到真正用好 Milvus,这条路并不短。
分布式架构需要理解——Proxy、Coord、Node 各司其职,一条查询请求在系统内部如何流转?Schema 怎么设计、Chunk 策略怎么选、混合搜索怎么配?与 LangChain、LangGraph 怎么集成?生产环境更是另一个战场——内存优化、写入调优、慢查询排查,每一个都是实打实的工程问题。
为了系统性地解决这些问题,我们整理了这份 《Milvus Workshop:从入门到应用》。
这份教程已经在多场线下 Workshop 中经过验证,帮助数百位开发者快速上手 Milvus。现在,我们上线了网页版——打开浏览器就能学,手机、平板、电脑无缝切换。
这份教程适合谁?
向量数据库新手:想系统入门,建立完整的知识体系
RAG/Agent 开发者:需要将 Milvus 集成到项目中,希望少走弯路
准备上生产的团队:开发环境已经跑通,需要了解生产环境的注意事项
想深入理解 Milvus 底层的工程师:不满足于会用 API,想搞清楚底层原理
Web 版上线:降低学习门槛
之前这份教程只有 GitHub 仓库版本,需要 clone 下来用 Jupyter 打开。代码能跑,但阅读体验受限,尤其在移动端几乎无法使用。
现在我们用 GitHub Actions 自动构建了 Web 版本:
零配置访问:无需 clone 仓库,无需配置 Python/Jupyter 环境,浏览器直接阅读
移动端适配:响应式设计,手机、平板、电脑无缝切换
自动同步更新:仓库更新后网站自动重新构建
通勤时间看架构原理,工作时间跑代码实操,学习效率显著提升。
教程结构
整个 Workshop 分为四大模块,每章都以 Jupyter Notebook 格式呈现,所有代码可直接运行。同时也有对应的 Web 版本。
👉 想直接开始? 点击这里打开教程
Part 1:核心概念与架构原理
1.1 向量数据库的本质
传统数据库擅长精确匹配:给定 ID 查记录,给定条件筛数据。但面对”哪些数据与这个最相似”这类问题,传统方案力不从心。
向量嵌入(Vector Embedding)将语义转化为数学表示。通过 Embedding 模型(如 BGE、E5、OpenAI text-embedding-3 等),文本、图片、音频等非结构化数据被映射为高维向量。语义相近的内容,向量空间中的距离也相近。
向量数据库的核心任务:在海量向量中高效检索最相似的 Top-K 结果。
精确的最近邻搜索需要遍历所有向量,数据量越大查询越慢,无法满足大规模数据的性能要求。ANN(Approximate Nearest Neighbor)通过构建索引结构,以可接受的精度损失换取数量级的性能提升。
1.2 索引类型对比
索引类型
技术特点
适用场景
FLAT(暴力搜索)
精度 100%,无近似误差
小数据量、精度要求极高
IVF 系列(IVF_FLAT / IVF_SQ8 / IVF_PQ)
聚类分桶,量化压缩
中等精度、中等吞吐
HNSW
多层图结构,低延迟
高吞吐 + 高召回场景,适合推荐、搜索
DiskANN
基于图的磁盘索引
大数据量,有限内存场景,牺牲部分延迟
GPU_IVF 系列
GPU 加速版 IVF
高吞吐、高并发场景,极致性能优化
BIN_FLAT / BIN_IVF_FLAT
二进制向量支持
文本、音频哈希类场景
IVF 系列是经典的聚类索引,通过将向量分桶实现快速检索,配合量化压缩可显著降低内存占用。HNSW 基于 Hierarchical Navigable Small World 图结构,查询性能优异,是当前最流行的索引类型之一。DiskANN 则适合内存受限但数据量大的场景,是性价比之选。
1.3 Milvus 分布式架构
Milvus 采用存储计算分离的云原生架构,分为四层:
Access Layer:Proxy 节点负责接收请求、参数校验、路由分发。无状态设计,支持水平扩展。
Coordinator Layer:集群协调中心,包含四个组件:
Root Coord:管理集群拓扑、DDL 操作、时间戳分配
Data Coord:协调数据写入流程,管理 Segment 分配
Query Coord:管理查询负载均衡,决定数据加载策略
Index Coord:调度索引构建任务
Worker Layer:执行层节点:
Data Node:处理数据写入和持久化
Query Node:加载数据和索引,执行搜索查询
Index Node:执行索引构建任务
Storage Layer:
Meta Storage:etcd 存储元数据
Log Broker:Pulsar/Kafka 作为 WAL
Object Storage:MinIO/S3 存储数据文件
Milvus 2.6 引入了 Streaming Node,专门处理实时数据流,进一步优化写入性能。
理解这套架构,对后续的问题排查和性能调优至关重要。
Part 2:Python SDK 实战
2.1 Schema 设计要点
创建 Collection 时的关键决策:
主键类型:Int64 或 VarChar,是否启用 auto_id
向量维度:取决于 Embedding 模型(BGE-M3 1024,OpenAI text-embedding-3-small 1536,E5-large 1024)
标量字段:确定哪些属性需要用于过滤查询
Schema 设计直接影响后续的查询效率和灵活性,建议在编码前充分规划。
2.2 HNSW 索引参数
三个关键参数:
M:每个节点的最大连接数,通常取 8-64,越大召回率越高但内存占用也越大
efConstruction:构建时的搜索宽度,通常取 100-500,越大索引质量越好但构建时间越长
ef:查询时的候选集大小,越大召回率越高但查询越慢
参数配置对性能影响显著,建议通过基准测试确定最优值。
2.3 搜索方式
向量搜索:
1
2
3
4
5
6
7
results = collection.search(
data=[query_vector],
anns_field="embedding",
search_params={"metric_type": "COSINE", "params": {"ef": 64}},
limit=10,
output_fields=["title", "content"]
)
带标量过滤的搜索:
1
2
3
4
5
6
7
8
results = collection.search(
data=[query_vector],
anns_field="embedding",
search_params={"metric_type": "COSINE", "params": {"ef": 64}},
limit=10,
filter="category == 'tech' and publish_date > '2024-01-01'",
output_fields=["title", "content"]
)
混合搜索:Milvus 2.4+ 支持同时使用稠密向量和稀疏向量(BM25),结合语义相似性和关键词匹配,通常能获得更好的检索效果。
Part 3:应用场景实战
3.1 跨模态图片搜索
CLIP 模型能够将图片和文本映射到同一向量空间,实现跨模态检索。
实现流程:
离线阶段:使用 CLIP 图像编码器将图片库转换为向量,存入 Milvus
在线阶段:用户输入文本描述,使用 CLIP 文本编码器生成向量,检索最相似的图片
教程提供完整代码,涵盖模型加载、批量处理、检索实现。
3.2 RAG 检索增强生成
RAG 是当前最主流的 LLM 应用模式,核心思路:先检索相关知识,再基于检索结果生成回答。
完整流程:
文档分块:将长文档切分为 500-1000 字符的片段
向量化:使用 Embedding 模型将每个片段转换为向量
存储:将向量和原文存入 Milvus
检索:用户提问时,将问题向量化,检索最相关的片段
生成:将检索结果作为上下文,由 LLM 生成回答
以下是 LangChain 集成示例:
1
2
3
4
5
6
7
8
9
# 检索相关文档
docs = vectorstore.similarity_search(query, k=5)
# 构建上下文
context = "\n".join([doc.page_content for doc in docs])
prompt = f"基于以下内容回答问题:\n{context}\n\n问题:{query}"
# 生成回答
response = llm.invoke(prompt)
关键优化点:
Chunk 策略:片段过小会丢失上下文,过大会引入噪音
Embedding 模型选择:BGE、E5、OpenAI text-embedding-3 各有特点,需根据场景选择
混合搜索:结合稠密向量和 BM25 稀疏向量,提升专有名词和关键词的检索效果
Rerank:使用 Cross-Encoder 对初筛结果重排序,进一步提升相关性
教程使用 LangChain 实现完整流程,并详细讨论这些优化策略。
3.3 AI Agent 记忆系统
Agent 的三大核心能力:规划(Planning)、记忆(Memory)、工具(Tools)。Milvus 在记忆系统中发挥关键作用。
短期记忆:当前会话的上下文信息,支持对话过程中的信息检索。
长期记忆:历史对话、执行经验、学习成果的持久化存储。遇到相似场景时,Agent 可以检索相关记忆,做出更优决策。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 存储对话记忆
memory_vector = embedding_model.encode(conversation_summary)
collection.insert({
"vector": memory_vector,
"content": conversation_summary,
"timestamp": datetime.now().isoformat(),
"type": "conversation"
})
# 检索相关记忆
relevant_memories = collection.search(
data=[current_query_vector],
anns_field="vector",
limit=5,
filter="type == 'conversation'"
)
教程使用 LangGraph 实现带记忆功能的 Agent,演示记忆的存储、检索和应用。
Part 4:生产环境运维
4.1 可观测性方案
Milvus 原生支持 Prometheus 指标暴露。教程提供完整的可观测性方案:
Prometheus:采集各组件指标
Loki:日志收集
Jaeger:分布式链路追踪
Grafana:可视化仪表盘
提供开箱即用的 Dashboard JSON,涵盖 QPS、延迟、内存使用、Segment 状态等关键指标。
4.2 性能基准测试
VectorDBBench 是 Zilliz 开源的向量数据库基准测试工具。上线前建议进行完整的性能测试,了解系统的 QPS 上限和延迟分布。
4.3 性能调优指南
内存优化:
调整 dataNode.segment.maxSize 控制 Segment 大小
使用 IVF_PQ 等压缩索引减少内存占用
按需 Load Partition,避免加载不必要的数据
写入优化:
批量插入,每批 1000-10000 条
大数据量场景使用 Bulk Import
合理配置 dataNode.flush.insertBufSize
查询优化:
根据数据规模选择合适的索引类型和参数
优化标量过滤条件,避免全表扫描
合理使用 Partition 缩小查询范围
4.4 版本升级
教程包含 Milvus 2.5 到 2.6 的升级指南,涵盖升级步骤和注意事项。
获取方式
传统方式
Web 版
环境要求
Git + Python + Jupyter
浏览器
移动端体验
受限
完整支持
内容更新
手动 pull
自动同步
📖 在线阅读:https://airag.click/milvus-workshop/
💻 GitHub 仓库:https://github.com/richzw/milvus-workshop
教程提供中英双语版本,欢迎 Star ⭐ 支持。我们将持续更新更多实战案例和最佳实践。
向量数据库的时代已经到来。无论是入门学习还是生产实践,这份教程都能提供有价值的参考。
Happy hacking 🚀
---
## 懒猫微服实战入门(三十三):网络设置中不显示我的WIFI怎么办?
- URL: https://blog.no-claw.com/posts/ff23bf7c/
- 日期: 2025-11-21
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服
折腾网络这件事,有时候比修电脑还玄学。
前几天测试家里的网络,顺手重置了一下路由器。结果没想到,懒猫微服的 Wi-Fi 设置也跟着丢了。
打开网络设置一看,满屏都是邻居家的热点,就是没有我自己的。
没关系,懒猫毕竟是 Linux 系统,只要能够 开启 SSH,命令行就是万能钥匙。反正有分层文件系统,真要捣鼓坏了,重启也能恢复。
这次的主角是 nmcli,全称 NetworkManager Command Line Interface,它是 Linux 自带的网络管理命令行工具。
通俗点说,它是前端界面的“幕后操控者”——我们在图形界面上点击的“网络连接”“Wi-Fi 设置”,其实都在底层调用它。
有了 nmcli,我们几乎可以用命令完成所有网络操作:
功能
命令示例
列出可用 Wi-Fi 热点
nmcli device wifi list
连接 Wi-Fi
nmcli device wifi connect "SSID" password "12345678"
查看当前网络状态
nmcli connection show --active
启用/禁用网卡
nmcli device set wlan0 managed yes/no
断开网络连接
nmcli connection down id "MyWiFi"
扫描 Wi-Fi
先 SSH 登录懒猫微服,直接使用 nmcli 扫描周边的 Wi-Fi:
1
nmcli device wifi list
这条命令会列出当前设备能检测到的所有 Wi-Fi 热点。
果然,我的 Wi-Fi 就静静地躺在输出列表里,只是前端页面没显示出来而已。
手动连接 Wi-Fi
既然能看到 SSID,那就直接连接:
1
sudo nmcli device wifi connect "MyWiFi" password "12345678"
几秒钟后,终端提示:
1
Device 'wlp129s0' successfully activated with 'xxxx-xxxx-xxxx'
说明 Wi-Fi 已成功连接,懒猫顺利回到网络世界。
隐藏密码的连接方式
前面的命令会在命令行里明文显示密码。
其实 nmcli 也支持交互式连接,输入命令后系统会自动提示输入密码:
1
sudo nmcli --ask device wifi connect "MyWiFi"
这种方式既安全又方便,命令行历史不会留下明文密码。
确认连接状态
连接成功后,可以用以下命令验证当前网络状态:
1
nmcli connection show --active
输出会显示所有活跃连接,包括 Wi-Fi、以太网、Docker 桥接等:
1
2
3
NAME UUID TYPE DEVICE
Wired connection 1 14c5aa14-82e5-33f6-be2b-04c6de5bfe58 ethernet enp2s0
GL-MT3600BE-236-5G 1 a2b7d70d-09f5-459f-9903-dd140b1e0d33 wifi wlp129s0
回到懒猫网络设置页面,就能看到熟悉的 Wi-Fi 已经连接上啦。
一键断开 Wi-Fi(真·命令行版)
想断开连接也很简单,只要执行:
1
nmcli connection down id "GL-MT3600BE-236-5G 1"
系统会返回:
1
Connection 'GL-MT3600BE-236-5G 1' successfully deactivated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/17)
轻松优雅,不需要点来点去。
---
## 零代码改动!用 Docker 将 Flask 应用部署到 AWS Lambda
- URL: https://blog.no-claw.com/posts/79854e67/
- 日期: 2025-11-19
- 分类: 软件 / AWS
- 标签: AWS, Docker
你有一个现成的 Flask API,想部署到 AWS Lambda 享受 Serverless 的好处,但又不想改代码?AWS Lambda Web Adapter 可以帮你实现。
本文将手把手教你如何使用 Docker + Gunicorn + Lambda Web Adapter,将 Flask 应用部署到 Lambda,并通过 API Gateway 对外提供服务。
为什么选择这个方案?
传统方式部署 Flask 到 Lambda 需要使用 Mangum、aws-wsgi 等第三方库,需要修改代码添加 handler。而 Lambda Web Adapter 是 AWS 官方方案,有以下优势:
零代码改动:Flask 代码完全不用改
生产级配置:可以使用 Gunicorn 作为 WSGI 服务器
本地开发友好:同一个 Docker 镜像本地和 Lambda 都能跑
框架无关:Flask、Django、FastAPI 都支持
架构概览
1
2
3
4
5
客户端 → API Gateway → Lambda (Docker 容器)
↓
Lambda Web Adapter
↓
Gunicorn + Flask
Lambda Web Adapter 作为 Lambda Extension 运行,负责将 API Gateway 事件转换为标准 HTTP 请求,Flask 应用完全感知不到自己运行在 Lambda 上。
准备工作
确保你已安装:
Docker
AWS CLI(已配置凭证)
Python 3.11+
第一步:创建 Flask 应用
创建项目目录和文件:
1
mkdir flask-lambda && cd flask-lambda
app.py - 一个简单的 Flask API:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from flask import Flask, jsonify, request
app = Flask(__name__)
@app.route('/')
def health():
return jsonify(status='healthy')
@app.route('/api/hello')
def hello():
name = request.args.get('name', 'World')
return jsonify(message=f'Hello, {name}!')
@app.route('/api/echo', methods=['POST'])
def echo():
data = request.get_json()
return jsonify(received=data)
requirements.txt:
1
2
flask==3.0.0
gunicorn==21.2.0
第二步:配置 Gunicorn
gunicorn.conf.py - 针对 Lambda 优化的配置:
1
2
3
4
5
6
7
8
bind = '0.0.0.0:8080'
workers = 1 # Lambda 单实例,1 个 worker 足够
threads = 4 # 多线程处理并发
timeout = 30
keepalive = 2
accesslog = '-' # 输出到 stdout,方便 CloudWatch 收集
errorlog = '-'
loglevel = 'info'
第三步:编写 Dockerfile
Dockerfile:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM python:3.11-slim
# 添加 Lambda Web Adapter
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.9.1 /lambda-adapter /opt/extensions/lambda-adapter
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8080
CMD ["gunicorn", "-c", "gunicorn.conf.py", "app:app"]
核心就是这一行 COPY --from=...,它从 AWS 公共 ECR 仓库拉取 Lambda Web Adapter 二进制文件,放到 /opt/extensions/ 目录。Lambda 启动时会自动加载这个 Extension。
第四步:本地测试
构建前先登录 ECR Public:
1
2
aws ecr-public get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin public.ecr.aws
构建并运行:
1
2
docker build --platform linux/amd64 -t flask-lambda .
docker run -p 8080:8080 flask-lambda
测试:
1
2
3
4
5
6
7
8
9
10
curl http://localhost:8080/
# {"status":"healthy"}
curl "http://localhost:8080/api/hello?name=Lambda"
# {"message":"Hello, Lambda!"}
curl -X POST http://localhost:8080/api/echo \
-H "Content-Type: application/json" \
-d '{"msg":"hello"}'
# {"received":{"msg":"hello"}}
本地没问题,接下来部署到 AWS。
第五步:推送镜像到 ECR
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 设置变量
AWS_REGION="ap-northeast-1"
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
ECR_REPO="flask-lambda"
# 创建 ECR 仓库
aws ecr create-repository --repository-name $ECR_REPO --region $AWS_REGION
# 登录 ECR
aws ecr get-login-password --region $AWS_REGION | \
docker login --username AWS --password-stdin \
$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com
# 打标签并推送
docker tag flask-lambda:latest \
$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPO:latest
docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPO:latest
第六步:创建 Lambda 函数
首先创建 IAM 角色:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 创建信任策略
cat > trust-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}
EOF
# 创建角色并附加权限
aws iam create-role \
--role-name flask-lambda-role \
--assume-role-policy-document file://trust-policy.json
aws iam attach-role-policy \
--role-name flask-lambda-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
sleep 10 # 等待角色生效
创建 Lambda 函数:
1
2
3
4
5
6
7
8
aws lambda create-function \
--function-name flask-api \
--package-type Image \
--code ImageUri=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPO:latest \
--role arn:aws:iam::$AWS_ACCOUNT_ID:role/flask-lambda-role \
--timeout 30 \
--memory-size 512 \
--region $AWS_REGION
第七步:配置 API Gateway
打开 API Gateway 控制台,创建 REST API:
点击「创建 API」→ 选择「REST API」→「构建」
输入 API 名称,如 flask-api
创建资源:
点击「创建资源」
勾选「代理资源」
资源路径填 {proxy+}
点击「创建资源」
设置集成:
集成类型选「Lambda 函数」
勾选「Lambda 代理集成」
选择你的 Lambda 函数 flask-api
同样为根路径 / 创建 ANY 方法,集成到同一个 Lambda
点击「部署 API」,阶段名填 v1
部署完成后,你会得到一个调用 URL,类似:
1
https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/v1
第八步:测试 API
1
2
3
4
5
6
7
API_URL="https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/v1"
curl $API_URL/
# {"status":"healthy"}
curl "$API_URL/api/hello?name=Serverless"
# {"message":"Hello, Serverless!"}
🎉 大功告成!你的 Flask API 已经运行在 Lambda 上了。
进阶:IAM 认证
如果你的 API 需要认证,可以在 API Gateway 中启用 IAM 认证。调用时需要对请求进行 SigV4 签名:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import boto3
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
import requests
def signed_request(method, url, data=None, headers=None):
session = boto3.Session()
credentials = session.get_credentials()
headers = headers or {}
request = AWSRequest(method=method, url=url, data=data, headers=headers)
# 注意:API Gateway 用 execute-api,Function URL 用 lambda
SigV4Auth(credentials, "execute-api", "ap-northeast-1").add_auth(request)
return requests.request(
method=method,
url=url,
headers=dict(request.headers),
data=data
)
resp = signed_request("GET", f"{API_URL}/api/hello")
print(resp.json())
更新部署
代码更新后,只需重新构建镜像并更新 Lambda:
1
2
3
4
5
6
7
docker build --platform linux/amd64 -t flask-lambda .
docker tag flask-lambda:latest $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPO:latest
docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPO:latest
aws lambda update-function-code \
--function-name flask-api \
--image-uri $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPO:latest
冷启动优化
首次请求可能需要 1-2 秒(冷启动),可以通过以下方式优化:
启用 Provisioned Concurrency:预热实例,消除冷启动
减小镜像体积:使用 slim 基础镜像,减少依赖
启用异步初始化:设置环境变量 AWS_LWA_ASYNC_INIT=true
总结
使用 Lambda Web Adapter,你可以:
保持 Flask 代码不变
使用熟悉的 Gunicorn 生产配置
同一镜像本地和云端都能运行
享受 Serverless 的弹性伸缩和按需付费
参考资料
AWS Lambda Web Adapter GitHub
Lambda 容器镜像支持
API Gateway Lambda 代理集成
---
## 懒猫微服实战入门(三十一):百度网盘迁移到懒猫网盘
- URL: https://blog.no-claw.com/posts/5332ecc4/
- 日期: 2025-11-01
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服
拿到一台新的 NAS,大家往往第一件事不是装 Docker、也不是跑容器,而是——把原先公有云上的数据,迁回到自己的私有空间里。
这是“云端回家”的第一步。对很多人来说,NAS 不仅是一个硬盘,更是重新掌握数据主权的起点。
这篇文章就以最常见的百度网盘为例,演示如何将文件迁移到懒猫网盘中。
我们会介绍两种方式:
使用自带的百度网盘 NAS 版本套件(最直接);
使用SMB 网络共享挂载的方式实现中转下载(更灵活)。
一、为什么迁移?
很多人习惯把资料放在百度网盘、腾讯微云、甚至 iCloud 上,但这些服务有一个共同点:你的数据都在别人的服务器上。
无论是隐私、速度,还是访问的自由度,都无法与自建 NAS 相比。
而懒猫网盘正好提供了一个简洁稳定的私有云方案:支持 SMB、WebDAV、HTTP 访问,同时能在内网高速读写,甚至还能开放公网远程访问。
因此,拿到 NAS 后,把百度网盘里的资料迁移过来,就成了多数用户的首要任务。
方法一:使用百度网盘 NAS 版套件
懒猫商店中自带的百度网盘 NAS 版本,其实是从群晖 Synology 移植过来的。
它提供了基础的上传、下载、同步功能,对于多数人来说已经足够。也无需再购买百度网盘 NAS 版本会员。
安装完成后,你会在 NAS 的根目录下看到一个名为 BaiduNetDiskNas 的文件夹。
这是百度网盘 NAS 套件默认的数据目录,所有通过该套件下载的文件都会存放在这里。
和其他套件不同,百度网盘的数据不会混进应用路径,而是直接落在 NAS 的根目录下,路径清晰,也方便后续迁移或备份。
使用方式很简单:
打开百度网盘 NAS 应用,登录账号;
选择要下载的文件;
点击下载即可。
这种方式不需要额外配置,也不用动网络端口,适合初次上手懒猫网盘的用户。
因此,如果你希望速度更快、结构更灵活,可以继续看下面的第二种方案。
方法二:通过 SMB 挂载中转迁移
另一种更通用的方案,是使用 SMB(Samba)共享的方式。
简单来说,就是让你的电脑直接访问懒猫网盘的共享目录,把它当成本地磁盘使用。
这样一来,在百度网盘客户端中,就可以直接把下载路径设为 NAS 上的 SMB 共享文件夹,下载完成即同步到 NAS。
具体步骤如下:
1. 启用 NAS 的 SMB 服务
在懒猫网盘中,进入【网盘 → 网络服务】,找到 SMB 服务内网开关,将其打开。
如果你有公网访问的需求,也可以使用懒猫提供的默认域名来挂载 SMB。
2. 在电脑上连接 NAS 共享目录
以 macOS 为例:
打开 Finder(访达) → 顶部菜单栏点击「前往」→「连接服务器…」
输入地址,例如:
1
smb://192.168.1.4/用户名
如果你希望直接连接到某个子目录,也可以在后面加上路径,比如:
1
smb://192.168.1.4/用户名/文件夹的名字
点击连接后,系统会提示输入用户名和密码。输入你在懒猫网盘中提示的账户信息即可。
连接成功后,你会在 Finder 的侧边栏看到 NAS 的共享文件夹。
3. 在百度网盘中选择下载路径
打开百度网盘客户端 → 设置 → 下载路径 → 手动选择
此时选择你刚刚挂载的 NAS 共享目录。
我这里建了一个名为 “百度” 的文件夹,用于专门存放网盘迁移文件。
这样,百度网盘在下载文件时,实际上是直接把数据写进懒猫网盘中。
整个过程既不占用本机空间,也不需要二次传输,非常高效。
四、两种方式的对比
对比维度
百度网盘 NAS 套件
SMB 中转迁移
安装难度
一键安装
需配置 SMB 服务
下载速度
一般(受限于百度接口)
取决于本地带宽,通常更快
文件路径
固定在 BaiduNetDiskNas
可自定义结构
适合人群
新手用户
进阶/高需求用户
依赖环境
NAS 端运行
NAS + 本地 PC 配合
总结一下:
如果你只想简单备份数据,百度网盘 NAS 套件足够;
如果你希望文件即时同步、可控性更强,SMB 中转方式更值得使用。
写在最后
公有云的便利和便宜,确实让很多人习惯把所有资料交给它。
但当你真正拥有一台 NAS、部署起懒猫网盘那一刻,就会发现——数据掌握在自己手里的安全感,是任何会员都买不到的。
迁移数据只是第一步,之后你还可以:
开启内网同步,让所有设备自动备份;
部署媒体服务器,打造自己的影音库;
甚至用 Docker 搭建 Git、PhotoPrism、Plex 等完整生态。
无论你是第一次接触 NAS,还是从群晖、威联通迁移过来的老用户,希望这篇教程能帮你顺利完成“云端回家”的第一步。
---
## 懒猫微服实战入门(三十二):让懒猫微服替你看家,摄像头接入篇(上)
- URL: https://blog.no-claw.com/posts/88c99791/
- 日期: 2025-11-01
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服
江湖上有句话——“能被自己掌控的数据,才算真正的安全”。
很多人买了摄像头,却不知录像都在谁手里。
我倒宁可折腾一点,也要让录像回到自己掌管的地盘里。
毕竟人在江湖漂,出门在外,男孩子也得懂得保护好自己。
所以这次,我干脆把家里的摄像头,直接接入了懒猫微服。从此录像不走云,不怕会员断。
——这是懒猫微服的又一次实战升级。
为什么要接入懒猫微服
如今的摄像头几乎都自带云存储功能,但云端录像的痛点大家都懂:
要充会员,不然只能存储存卡,导出来很麻烦;
无法掌控数据,云厂商想删你一点办法都没有;
延迟与限速,下载一段视频比登天还慢。
而懒猫微服就像一个沉稳的大侠,守得住隐私,也能接得住设备。它本质是一台私人云服务器,只要设备支持主流协议(SMB、RTSP 等),都能被纳入懒猫的怀抱——我今天接入的是一款中兴的摄像头。
开启懒猫网盘 SMB 共享
摄像头原生支持 SMB 协议,这意味着它能直接把录像写进 NAS。
我们只需在懒猫微服的后台中,开启对应的服务即可:
路径:【网盘 → 网络服务 → SMB 服务内网开关】
懒猫的 SMB 模块非常稳定,能同时兼容 Windows、macOS、摄像头、甚至路由器的共享写入。
只要设好账号密码、IP 授权范围,就能实现局域网高速传输。
摄像头接入 SMB
打开摄像头 App(我的是中兴智慧生活),
选择 “NAS 网络存储” 功能,并点击“手动添加”。
在地址栏中输入懒猫网盘的 IP 和账号凭证即可。一顿折腾之后终于可以了,中兴的程序员要上点心,抛异常要写具体,不管是密码不对还是啥错都报错不能匿名登陆。这样很误导人,真的有很多开发连 401 和 403 都傻傻分不清。
懒猫网盘开始接管录像
当摄像头成功连接后,你会在懒猫网盘里看到一个新文件夹——录像开始一段段被写入 NAS 中。
我这个摄像头录出来的文件是 MP4 格式,这点非常友好。懒猫网盘甚至能直接在线播放,也支持直接下载离线观看。如果想做 AI 识别或画面检索,还能直接喂进懒猫商店里的其他应用做视频分析。
由于我开了全天录制,记录保持一年,这个算是摄像头能够支持最大的限度了。可以看到懒猫网盘中一天的录像占用 7G 多一点,反正都有 NAS 了,算下来一年也就占用 2.6T,完全放得下,这可是用内存卡办不到的,借助懒猫微服的大容量存储,我们可以随时回滚到一年前的记忆。
从此我不必担心内存卡满、云端过期、账号异常。
所有录像都牢牢存在我自己的懒猫微服里,随时取、随时删、随时回看。
尾声
懒猫微服能接入的不止摄像头,它能接入的,是生活的秩序、家的安全感,以及我们对数字世界一点点的掌控力。
江湖很大,懒猫很静。
懒猫微服,不只是安静地趴在角落里。它能听、能看、能存、能守——是我出门在外最可靠的“家伙”。
折腾这些配置的过程,就像练武:一开始总会走火入魔,但掌握之后,心中自有一份安稳。
懒猫微服接入摄像头这件事,也许不是什么大事,但它让我更笃定——折腾,是生活的一种浪漫。
---
## Easysearch 更新后日志中看不到密码?一文带你搞清楚原因与解决方案
- URL: https://blog.no-claw.com/posts/dba72588/
- 日期: 2025-10-19
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
最近在群里看到不少朋友反馈,Easysearch 升级到某个版本之后,日志文件里不再能看到初始化密码了。以前版本我们可以轻松在 /app/easysearch/logs/initialize.log 中找到,比如 exec 进入容器后直接 grep curl 搜索 Easysearch URI 字段,就能定位密码所在行。但现在——无论是 grep 还是手动翻,都空空如也。
问题现象:日志里“密码不见了”
过去版本,Easysearch 初始化时会将自动生成的默认密码打印到日志文件中。
如下图所示,这样的日志路径在老版本中非常常见:
但在新版中,这条日志记录已经消失。
我平常习惯用 Dockage 来拉起 docker-compose,但由于日志滚动过快,输出信息一多也容易被覆盖。
02. 官方确认:这是出于安全考虑
在 Easysearch 的官方交流群中咨询后,得到了 CEO 本人的亲自回复:
新版之所以不再在日志中输出密码,是为了提高安全性,防止明文凭证泄露。
换句话说,密码仍然会在启动过程中生成,只是不再被重定向到容器内部的日志文件。
这意味着我们要换个思路,从 Docker 运行日志 中找密码。
查看启动日志中的密码
新版的密码信息仍会在启动时输出,只是没落地到文件。
所以我们可以直接用 Docker 命令查看:
1
docker logs | grep password
在 macOS 上,用 Obstack 或 Dozzle 这样的图形化 Docker 日志工具也能非常直观地查看输出:
不过要注意:如果容器重启过多次,历史日志会被覆盖,因此建议在第一次启动时就及时复制密码。
更稳妥的方案:自定义初始化密码
其实完全可以避免手动找密码的麻烦——直接在启动 Easysearch 时自定义密码。
参考我之前的文章:
👉 启动 Easysearch 时自定义密码的操作方法
这样在 CI/CD 或本地部署时都能保持一致的密码配置,避免每次重启都要重新查找。
忘记密码?还可以重置!
如果你已经错过了初始化日志,又没配置自定义密码,也不用慌。
热心群友提供了铭毅天下发布的官方重置方法,实测可行:
👉 Easysearch 重置密码的办法
重置完成后系统会生成一个新的密码。
执行验证命令时,如果密码里有 !,要注意 zsh 的特殊行为。
zsh 会把 ! 当作“历史命令展开符”,不转义会直接报错。
正确的写法如下:
1
curl -ku admin:'!8We9L6@g6!NMZpEDx2Apn6U' https://localhost:9200
或者使用反斜杠转义:
1
curl -ku admin:\!8We9L6@g6\!NMZpEDx2Apn6U https://localhost:9200
总结
新版 Easysearch 不再在日志文件中明文输出密码,是出于安全强化的考虑。
要找密码,可以:
用 docker logs 查看启动日志;
启动时通过环境变量自定义密码;
若遗忘,可使用官方 reset 方法重置。
虽然麻烦了一点,但这确实是更安全、更企业化的做法。
---
## 懒猫微服开发篇(八):反向代理与自签名 SSL 的优雅共存之道
- URL: https://blog.no-claw.com/posts/990eab20/
- 日期: 2025-10-07
- 分类: 懒猫微服 / 开发
- 标签: Docker, 懒猫微服
在分布式系统中,HTTPS 是默认的通信规范。但当你在内部环境中部署服务时,特别是很多以容器方式运行的组件,但是很多时候
它们往往只默认开启了 HTTPS 接口,却附带了一个 自签名证书(Self-signed certificate)。
这意味着,只要一层反向代理(reverse proxy)去转发 HTTPS 请求,TLS 校验就会报错。
这不是配置错误,而是 SSL 的“安全特性”在起作用。
本文就带大家看看如何在懒猫微服上架过程中如何优雅地解决这一问题。
问题背景:HTTPS 转发失败
假设我们要在懒猫微服上架一个 App,用来转发请求到后端的 Easysearch 搜索引擎。
项目的 manifest(lzc.yaml)配置如下:
1
2
3
4
5
6
7
8
9
lzc-sdk-version: "0.1"
name: proxy
package: cloud.lazycat.app.proxy
version: 0.0.1
application:
subdomain: proxy
routes:
- /=https://easysearch:9200/
这段配置的意思很简单:
当访问 proxy.lzcapp.xxx 时,请将请求转发到容器内的 https://easysearch:9200/。
然而在 Dozzle 日志中,你会看到熟悉的报错信息:
1
2
2025/10/08 10:20:15 http: proxy error: tls: failed to verify certificate: x509: certificate is valid for infini.cloud, *.infini.cloud, not easysearch
2025/10/08 10:20:17 http: proxy error: tls: failed to verify certificate: x509: certificate is valid for infini.cloud, *.infini.cloud, not easysearch
报错很直白:
证书仅对 infini.cloud 和 *.infini.cloud 有效,而不是 easysearch。
这正是 证书域名不匹配(SNI mismatch) 的典型例子。
原因分析:为什么 HTTPS 会报证书错
在 HTTPS 连接建立过程中,客户端(此处是反向代理)会验证服务器返回的证书:
证书是否过期;
是否由受信任的 CA 签发;
证书上的主机名是否与请求的主机名匹配。
而我们的容器内部通常使用服务名(例如 easysearch)访问,不是证书上写的正式域名(infini.cloud)。
这就造成了验证失败。
在浏览器中,如果你访问一个自签名 HTTPS,会看到类似提示:“连接不安全”。
在反向代理中,它不会弹窗,而是直接抛出异常中断。
Nginx 的特性:默认不验证上游证书
在以前的实验中,我用过 Nginx 来转发自签名 HTTPS 的 Easysearch,发现并没有报错。
这是因为——Nginx 默认不会验证上游 HTTPS 证书。
在官方文档 ngx_http_proxy_module 中明确写道:
Syntax: proxy_ssl_verify on | off; > Default: proxy_ssl_verify off; > Context: http, server, location
This directive sets whether to verify the SSL certificate of the proxied server.
也就是说:
默认情况下,proxy_ssl_verify 是 off;
Nginx 不会校验后端证书;
只有你显式开启 proxy_ssl_verify on; 时,它才会去核对证书域名、签发方、有效期等信息。
这点在内部网络部署中非常有用 —— 因为大部分内网服务使用的都是自签名或临时证书。
典型案例:Docker Compose + Nginx 反代 Easysearch
要更直观理解,我们先构造一个简单的实验场景。
下面是一个 Docker Compose 配置,启动一个 Easysearch 容器和一个 Nginx 容器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
version: "3.8"
services:
easysearch:
image: infinilabs/easysearch:1.15.3
container_name: es-single
ports:
- "9200:9200" # 本地直连(可选)
nginx:
image: nginx:latest
container_name: nginx-proxy
depends_on:
- easysearch
ports:
- "80:80" # 对外访问端口
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
Nginx 配置如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
events {}
http {
server {
listen 80;
server_name localhost;
location / {
proxy_pass https://easysearch:9200;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
这个配置里没有加任何 SSL 校验相关指令。
Nginx 在请求 https://easysearch:9200 时会直接忽略自签名问题,代理层不会抛错。
懒猫微服中的 OpenResty
懒猫微服默认 app.proxy 是 OpenResty(Nginx 增强版) 。
它天然具备和 Nginx 一样的 SSL 默认策略 —— 即忽略内网自签名的证书验证。
外挂的配置文件很麻烦,我们可以使用 setup_script 来执行 shell 命令来动态写入。效果和前面是一样的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
lzc-sdk-version: "0.1"
name: proxy
package: cloud.lazycat.app.proxy
version: 0.0.1
application:
subdomain: proxy
routes:
- /=http://app-proxy:80/
services:
app-proxy:
image: registry.lazycat.cloud/app-proxy:v0.1.0
setup_script: |
cat <<'EOF' > /etc/nginx/conf.d/default.conf
server {
listen 80;
server_name _;
location / {
proxy_pass https://easysearch:9200/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
EOF
easysearch:
image: registry.lazycat.cloud/u04123229/infinilabs/easysearch:5dc6296f5370aacc
这段配置中,setup_script 会在容器启动时执行,会动态写入 Nginx 配置文件。
容器之间的通信使用内网域名,不校验证书。这样代理转发就能成功,日志也不会再出现 x509 错误。
再优化:使用官方环境变量
如果你不想写配置文件,懒猫官方提供的 app-proxy 镜像还支持 UPSTREAM 环境变量 来简化操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
lzc-sdk-version: "0.1"
name: proxy
package: cloud.lazycat.app.proxy
version: 0.0.1
application:
subdomain: proxy
routes:
- /=http://app-proxy:80/
services:
app-proxy:
image: registry.lazycat.cloud/app-proxy:v0.1.0
environment:
- UPSTREAM="https://easysearch:9200"
easysearch:
image: registry.lazycat.cloud/u04123229/infinilabs/easysearch:5dc6296f5370aacc
这种方式更轻量,也便于在应用市场中复用。
UPSTREAM 会自动注入到镜像配置中,无需手动挂载文件。
进阶用法:新版本的 upstreams 支持 SSL 开关
懒猫微服新版本的 SDK 进一步增强了反向代理的灵活性。
你可以直接在 application 部分的 upstreams 中声明多个后端服务,并显式配置 SSL 行为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
lzc-sdk-version: "0.1"
name: proxy
package: cloud.lazycat.app.proxy
version: 0.0.1
application:
subdomain: proxy
upstreams:
- location: /
backend: https://easysearch:9200
disable_backend_ssl_verify: true # 关键参数:禁用 SSL 校验
services:
easysearch:
image: registry.lazycat.cloud/u04123229/infinilabs/easysearch:5dc6296f5370aacc
当 disable_backend_ssl_verify: true 设置后,平台在代理时会自动跳过证书校验步骤。
这相当于在 Nginx 中使用了:
1
proxy_ssl_verify off;
部署后日志如下:
1
2
3
PATH:"/" is served by {/ false https://easysearch:9200 false true false false [] false false []}
✨ Internal health check successful.
Standing by until other services are healthy. ⌛
代理成功生效。
结语
通过本文的两种方法,我们可以:
安全地代理 HTTPS 服务;
解决自签名证书报错;
保持部署流程一致、代码简洁;
同时兼顾上架懒猫商店的兼容性。
未来如果要接入多后端服务的多路复用机制也能轻松应对。
反向代理不仅是“流量的中转站”,更是“安全与兼容的缓冲层”。
理解 SSL 校验机制后,才能写出既安全又高可用的微服务架构。
---
## 铁威马:被忽视的中坚NAS玩家,为何在群晖阴影下沉默?
- URL: https://blog.no-claw.com/posts/1690f93c/
- 日期: 2025-10-07
- 分类: 电子产品 / 电脑 / NAS
- 标签: NAS
在个人与中小企业存储市场中,群晖(Synology)与威联通(QNAP) 几乎垄断了“NAS”的代名词。但如果你仔细观察京东、亚马逊或 B 站开箱视频,就会发现一个存在感极弱、但出货量其实不低的品牌——铁威马(TerraMaster)。
它产品线齐全、价格诱人、在亚马逊上好评不少,却始终无法在玩家社区中形成话题。为什么?本文试图从定位、生态、产品、传播与竞争格局五个维度剖析这家“沉默的 NAS 厂商”。
一、从硬盘盒到 NAS:铁威马的“工程师路线”
铁威马成立于 2002 年,最初做的是DAS(Direct Attached Storage)和 RAID 阵列柜,在 DIY 和视频剪辑圈里有一定口碑。
但它与群晖、威联通最大的区别是:
铁威马一直是一个“硬件思维主导的软件厂商”。
对比来看:
品牌
产品核心
技术策略
典型用户群
群晖 Synology
软件(DSM 系统)
打造生态闭环、重体验
家庭用户、中小企业
威联通 QNAP
软件 + 硬件多元化
丰富功能与性能路线
技术型企业用户
铁威马 TerraMaster
硬件性价比
以成本与兼容性为卖点
入门级、预算型用户
铁威马的 T 系列 NAS 硬件性能并不差,甚至同价位下 CPU、内存、网口规格往往更高,但它缺少软件体验层面的“惊喜”——这让它看起来更像“带系统的硬盘盒”,而不是一个“私有云平台”。
二、生态短板:TOS 系统的“孤岛效应”
铁威马自研的操作系统 TOS(TerraMaster OS)目前发展到 TOS 6.x,在 UI 设计与易用性上已有改进,但生态仍是最大痛点。
对比项目
群晖 DSM 7.x
威联通 QTS / QuTS
铁威马 TOS 6
系统界面
精致、流畅、统一
丰富但复杂
简单但略显老旧
应用中心
200+ 官方/第三方
300+ 插件生态
40+ 应用,更新慢
虚拟化支持
Docker、VM、Kubernetes
虚拟机中心、容器站
仅部分机型支持 Docker
安全更新
定期推送、活跃社区
补丁频繁
更新周期长、文档少
云服务集成
Synology Drive、Photos、C2
myQNAPCloud
CloudSync
结果是,TOS 虽然能满足基础文件共享、RAID 管理、Time Machine、备份任务等需求,但缺乏高级功能——例如群晖的 Active Backup、威联通的 QuMagie AI 照片识别、或多云同步整合。
这导致许多中高端用户在体验后觉得:
“功能够用,但系统太单调。”
三、品牌存在感弱:铁威马几乎不讲故事
群晖有论坛、有教程、有博主内容;威联通有 NAS 玩家社区、有 QTS 更新直播;
而铁威马呢?
除了官网更新日志和亚马逊评论,几乎没有声音。
缺乏社区意味着:
没有 UGC 教程或经验贴;
很少有 YouTube / B 站教学视频;
没有第三方开发者生态;
没有社群反馈机制。
这让铁威马在舆论层面处于“隐身”状态——普通用户不知道它,玩家不讨论它,企业客户不了解它。
甚至有种“用过就忘”的品牌感。
四、被市场挤压:上有群晖,下有自建 NAS
铁威马所处的价格区间(千元~三千元)正好是最卷的 NAS 市场。
但它的两侧都被强敌包夹:
上层被群晖压制
群晖以系统稳定、生态强著称,哪怕贵一倍,很多人依然选择它。
下层被自建 NAS 侵蚀
Unraid、TrueNAS、CasaOS 等方案越来越成熟,旧主机重生、N100 工控机当 NAS 成为趋势。铁威马的价格优势就不再突出。
于是,铁威马成了“想省钱又不想折腾”的那群人唯一的选项——但这群人,数量其实有限。
五、国际传播弱:营销与渠道的缺位
在全球市场上,铁威马的销售策略更接近传统 OEM 模式:靠亚马逊、电商分销、批发代理出货。
它几乎没有参与 Reddit、Discord、YouTube 技术社区的长期运营,也少有海外测评博主合作。
搜索“TerraMaster NAS”时,能看到的都是散乱的用户测评,而非系统的品牌叙事或技术展示。
而群晖、威联通早已形成“生态势能”:更新一个版本,都能被科技媒体报道。
六、但铁威马真的没有优点吗?
恰恰相反,它有几个被低估的优势:
💰 性价比极高:同价性能普遍优于群晖。
🧩 硬件开放度高:支持自装内存、第三方硬盘。
🛠 可玩性适中:支持 SSH、Docker、命令行操作。
🌐 企业存储产品线丰富:RAID 阵列柜、Thunderbolt DAS 方案。
如果你只是想:
备份电脑、相册;
做个小型文件服务器;
体验轻量 NAS;
铁威马依然是“买了不亏”的选择。
七、结语:被忽视的“安静力量”
铁威马的问题,不在产品,而在定位和传播。
它不是“做不好”,而是“不被看到”。
在一个重体验、重生态的市场中,仅靠“硬件性价比”已经难以立足。
铁威马需要做的不仅是提升性能,而是讲清楚自己是谁——
是要做“平价 NAS”,还是“轻企业私有云”?
是要成为入门市场的守护者,还是中小企业的替代方案?
否则,它可能继续像现在这样:
默默出货,默默被遗忘。
---
## Coco AI 插件商店很多功能,还能玩 2048
- URL: https://blog.no-claw.com/posts/76058f6a/
- 日期: 2025-10-06
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
在日常使用电脑的过程中,很多人习惯安装各种效率工具、启动器、播放器或日程管理应用。但这些功能其实都能被一个智能平台集中完成——这就是 Coco AI。
除了搜索和信息整理之外,Coco AI 还提供了一个完善的 插件系统。通过插件,它几乎可以控制电脑的方方面面:
操作本地备忘录、播放音乐、查找应用,甚至取代 macOS 的导航栏。更令人惊喜的是,它还内置小游戏,比如大家熟悉的 2048。
Coco AI 的系统整合能力
Coco AI 的插件系统不是简单的第三方扩展,而是深度整合在 macOS 桌面环境中的轻量级功能模块。
例如下图中的界面,插件可以直接访问系统级服务:
通过插件,Coco 能够直接读取或编辑本地备忘录、打开 Finder 文件夹、启动已安装的应用,甚至接管顶部导航栏,让桌面控制更加一体化。
在使用体验上,这些功能完全不需要额外配置。插件启用后,会自动加载到 Coco 的主控制界面,执行速度快,界面响应流畅。
二、音乐与媒体控制
音乐爱好者会发现,Coco 的插件生态对媒体控制支持得非常完善。
它不仅支持系统自带的音乐播放器,还能直接控制第三方软件,例如网易云音乐、QQ 音乐等。
在开启相关插件后,你可以在 Coco 面板中完成播放、暂停、切歌、调节音量等操作。
对于喜欢边工作边听歌的人来说,这种“中控台式”的体验比单独切换应用更加高效,也更美观。
三、桌面导航栏替代方案
除了控制音乐和笔记,Coco 还有一些插件能替代 macOS 顶部的系统导航栏。
得益于 Coco 的统一框架,导航栏可以和其他插件联动,这种深度整合的插件体验,使得 Coco AI 更像是一个“桌面操作系统的延伸层”,而不只是一个 AI 应用。
插件商店入口
在主界面右上角点击“加号”,就能打开 插件商店(Plugin Store)。
每个插件都有简介、开发者信息、版本号和一键安装按钮。安装后插件会立即生效,无需重启。
商店的搜索栏也很实用,可以直接输入关键词查找插件。例如输入 “2048”,就能找到一款完整的小游戏插件。
娱乐类插件:在 Coco 里玩 2048
工作之余,Coco 还可以成为一个放松工具。
在插件商店中搜索 “2048”,即可找到一款轻量化的本地小游戏。
安装完成后,打开 Coco 的插件区即可直接启动游戏。
整个界面与经典的 2048 完全一致,采用极简配色,操作流畅,不需要联网,也没有广告。
使用方向键控制方块合并,当数字达到 2048 时即可获胜。
游戏支持自动保存分数,下次打开可以继续上局,非常适合在短暂休息时消遣。
总结
Coco AI 的插件商店展示了一个轻量、开放、可扩展的桌面生态。
它将搜索、系统操作、音乐控制、任务管理、小游戏等多种功能融合在一个统一界面中。
对用户而言,Coco 的价值不仅在于“能干什么”,而在于“减少多少步骤”。
从打开备忘录、播放音乐,到临时玩一局 2048,所有操作都能在一个窗口内完成。
这种无缝衔接的体验,正在重新定义智能工具的形态——Coco 不再只是一个 AI 程序,而更像是 macOS 的「智能插件中心」。
---
## 摘抄:不要呆在没有话语权的小团体里
- URL: https://blog.no-claw.com/posts/8f725a10/
- 日期: 2025-10-04
- 分类: 摘抄 / B站大脑成长计划
- 标签: 摘抄
https://www.bilibili.com/video/BV1EQxKzAEtG/?spm_id_from=333.1391.0.0&vd_source=455daf3f8c90b0a5bd4160e8292c05f9
本视频探讨了人们在生活和工作中常常陷入缺乏话语权的小团体中的现象。这些小团体包括项目组、朋友聚会、家庭关系网等,表面上看似平等,但实际上存在权力关系,导致部分人的声音被忽视、观点不被重视。
关键点总结
话语权与权力关系:法国哲学家福柯指出,权力关系无处不在,即使在最微小的人际互动中也存在话语权,这是权力关系的体现。
边缘化的影响:长期处于缺乏话语权的环境会导致个体自我怀疑、认知扭曲,甚至形成“我的意见不重要”的错误认知。
温水煮青蛙效应:习惯于在团体中保持沉默,会导致思维肌肉萎缩,忘记如何为自己发声。
自我审查与内化沉默:在没有话语权的环境中,人们会不自觉地开始自我审查,开口前就先否定自己,这是对自我价值的背叛。
真实表达的重要性:一个不允许你真实表达的团体,不值得你付出宝贵的时间和情感。不平等的关系即使表面和谐,也难以长久。
尊重需求与自尊:心理学家马斯洛指出,尊重需求是人类的基本需求之一。在团体中被倾听和认可,是尊重的具体体现。长期得不到满足,将侵蚀自尊和自信。
寻找真正能让你发声的环境:离开那些让你失声的环境,不是逃避,而是对自我价值的坚守。要勇敢寻找能让你发声的场域,即使意见相左也能得到尊重。
自我价值与尊严:内心强大的人明白,与其在压抑的环境中获得表面接纳,不如在开放的氛围中享受真实对话。
声音的价值:西蒙娜·德·波伏娃指出,不要把你的声音囚禁在别人设定的框架里。每个人都有表达的权利,都值得被倾听。
人生选择:人生短暂而宝贵,不要把时间浪费在那些让你窒息的关系中。找到你的“部落”,在那里你的声音不仅能被听见,还能引起共鸣,甚至激发更多可能性。
社会性动物的定义:亚里士多德指出,人是社会性动物,但这并不意味着要忍受任何形式的社交关系,而是要寻找那些能让我们成长、让我们发光的连接。
核心信息
不要呆在没有话语权的小团体里,因为这会损害你的自我价值和尊严。
寻找能让你发声的环境,在那里你的声音会被听见,你的观点会被尊重。
人生短暂,不要浪费在让你窒息的关系中,找到真正能让你成长和发光的连接。
总结
本视频强调了在缺乏话语权的环境中长期停留的负面影响,并鼓励人们勇敢寻找能让自己发声、被尊重的环境。通过真实表达和被倾听,实现自我价值和内心的平静。
---
## 摘抄:我发现有个输出型爱好真的很重要
- URL: https://blog.no-claw.com/posts/e7ef5dac/
- 日期: 2025-10-04
- 分类: 摘抄 / B站大脑成长计划
- 标签: 摘抄
核心观点
视频强调了拥有一个输出型爱好对个人成长和心理健康的重要性。在信息爆炸的时代,人们往往陷入被动消费的循环,而忽视了主动创造的价值。通过输出,我们不仅能够将知识转化为自己的智慧,还能在创作过程中获得成就感和满足感,从而对抗现代生活的空虚与疲惫。
关键内容总结
1. 被动消费 vs 主动创造
现代人每天都在消费信息、娱乐和他人生活片段,却很少留下自己的痕迹。
信息洪流正在侵蚀我们的创造力和表达能力,使灵魂变得空洞和疲惫。
2. 输出型爱好的意义
输出型爱好(如写作、绘画、音乐、手工艺等)是将内心世界具象化的过程。
它不仅是技能的展示,更是思想的体现和情感的沉淀。
3. 输出带来的心理与认知益处
创造性输出可以激活大脑多个区域,促进认知整合,增强自我效能感。
完成作品后,大脑会释放多巴胺,带来成就感和满足感。
这种正向反馈机制有助于对抗现代生活的空虚与倦怠。
4. 输出型爱好与自我成长
输出型爱好不是专业技能,而是表达的真诚与坚持。
它帮助我们从被动消费转向主动创造,成为生活的导演而非观众。
通过持续的创作,我们不仅在塑造作品,也在塑造自己。
5. 历史与名人的启示
尼采说:“一个人必须拥有内心的混乱,才能孕育出一颗跳舞的星星。”
罗曼·罗兰说:“世界上只有一种英雄主义,就是认清生活真相后依然热爱生活。”
惠特曼说:“从今以后,我不再问‘我得到了什么’,而是问‘我能给予什么’。”
6. 如何开始输出型爱好
不必追求宏大,可以从每天写三句话、周末做蛋糕、为家人唱一首歌开始。
通过实践,我们会发现被动消费的时间变得乏味,而创造的时刻则更加生动和珍贵。
重要引用
“想象力比知识更重要。” —— 爱因斯坦
“我们通过实践获得我们所学的东西。” —— 亚里士多德
“如果一个人坚定地走向自己的梦想,努力活出他想象中的生活,他一定会取得意想不到的成功。” —— 梭罗
“我们不是世界的旁观者,而是自己故事的叙述者和创造者。” —— 惠特曼
总结
拥有一个输出型爱好,不仅是一种生活方式的转变,更是一种自我实现的途径。它让我们在信息洪流中保持清醒,在被动消费中找回主动创造的力量。从今天开始,尝试找到属于你的输出方式,让生活因创造而更加丰富多彩。
---
## 摘抄:改变自己最快的方式:早起+密集做事
- URL: https://blog.no-claw.com/posts/806d38a3/
- 日期: 2025-10-04
- 分类: 摘抄 / B站大脑成长计划
- 标签: 摘抄
https://www.bilibili.com/video/BV1mhTezyEhP/?spm_id_from=333.788.videopod.sections&vd_source=455daf3f8c90b0a5bd4160e8292c05f9
核心观点
改变自己的最快方式是早起并进行密集做事。通过早起,我们能够抓住一天中大脑最清醒的黄金时间,进行高效、专注的行动,从而实现自我提升和人生转变。
关键信息
1. 早起的意义
大脑最清醒:清晨是大脑最清醒、专注力最强的时刻,科学研究表明,此时的前额叶皮层最为活跃,意志力也达到一天的高峰。
掌控人生:早起意味着你选择掌控自己的生活,而不是被生活所控制。
时间价值:早起后的时间可以用于高效工作,使时间的价值成倍增长。
2. 密集做事的定义
高效专注:密集做事不是盲目忙碌,而是有目的、有计划地进行高强度工作。
多巴胺释放:专注工作时,大脑会释放多巴胺,提升学习效率并产生成就感,形成正向循环。
成功案例:一位从普通大学毕业的年轻人,通过每天早起并进行三小时的高强度工作,最终成功进入顶级投行。
3. 实践方法
建立早起习惯:
固定作息:提前设定睡觉时间,确保有足够睡眠。
设置闹钟位置:将闹钟放在床的另一端,迫使自己起床。
设计早晨仪式:如喝咖啡、听音乐、阅读等,让早起变得愉快。
使用番茄工作法:
每 25 分钟专注工作,休息 5 分钟,减少干扰。
研究表明,这种方法可提升工作效率至少 43%。
聚焦三件最重要的事:
每晚列出三件最重要的任务,确保在早晨高效完成。
管理大师彼得·德鲁克强调:效率是做对的事,效果是做重要的事。
4. 应对挑战
坚持策略:
小进步优于完美计划:即使只早起 10 分钟,也比放弃更好。
寻找同伴:与志同道合的人一起实践,互相支持和激励。
记录与反思:通过日记记录早起时间和完成任务,定期回顾进步与不足。
习惯的力量:
康德曾说:“习惯是人的第二本性”,一旦早起和高效工作成为习惯,改变将变得自然。
重要引用
罗曼·罗兰:“世界上只有一种英雄主义,就是在认清生活真相之后依然热爱生活。”
塞涅卡:“生命的长度不是由时间决定的,而是由你如何使用时间决定的。”
康德:“习惯是人的第二本性,它比自然更强大。”
管理大师彼得·德鲁克:“效率是做对的事,效果是做重要的事。”
爱因斯坦:“我们的行为塑造了我们的性格,而性格决定了我们的命运。”
卡耐基:“如果你想拥有从未拥有过的东西,你必须去做从未做过的事。”
总结
早起和密集做事是一种生活方式,也是一种对自我的承诺。它不仅改变了你的生活,也在重塑你的灵魂。从今天开始,让早起和高效工作成为你改变自己的力量,让每一个清晨都见证你迈向更好的自己。
---
## 摘抄:无事不要讲话,讲话只有三个目的
- URL: https://blog.no-claw.com/posts/78d33cb9/
- 日期: 2025-10-04
- 分类: 摘抄 / B站大脑成长计划
- 标签: 摘抄
核心观点
语言如刀:言语一旦说出,便难以收回,因此要谨慎使用。
沉默的力量:在信息爆炸的时代,沉默往往比言语更有价值。
有效言语的三大目的:传达信息、建立关系、推动行动。
关键信息
一、无效言语的危害
消耗精力:言语会占用有限的认知资源,使人疲惫。
损害信誉:过多言语让人产生免疫力,降低他人对你的信任。
暴露弱点:言语失控会暴露个人的弱点和秘密,影响人际关系和职场表现。
二、有效言语的三大目的
传达信息
语言最基本的功能是传递信息,但要精准传达关键内容。
优秀沟通者能用最简洁的语言表达最复杂的信息。
建立关系
语言是建立和维护情感连接的工具。
高情商的人懂得倾听,而非一味表达,建立更深层次的关系。
推动行动
有效言语能激发他人行动,产生实际改变。
例如,领导者的激励讲话、销售的促成技巧、父母的教育方式等。
三、如何做到言简意赅
言前思虑
说话前问自己三个问题:是否必要、是否合适、是否由我表达。
建立自我筛选机制,提升言语质量。
多听少说
人类有两只耳朵和一张嘴,应多听少说。
交谈中,倾听时间占 2/3,说话时间不超过 1/3。
避免情绪化表达
情绪激动时,理性思维下降,容易表达混乱。
遇到情绪波动时,应保持沉默,冷静后再表达。
总结
言语的价值:在信息过载的时代,言语应被珍惜,每句话都应有其意义。
真正的智慧:不在于说得多,而在于知道何时说、何时不说,以及如何用最精准的语言表达最有价值的思想。
成为有影响力的人:在喧嚣的世界中,做一个懂得珍惜言语的人,让每一句话都值得倾听,如金玉般珍贵。
---
## Easysearch 的写入流程(一):refresh
- URL: https://blog.no-claw.com/posts/eb05eb6d/
- 日期: 2025-10-04
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
在 Elasticsearch 或者 Easysearch 这样的搜索引擎中,写入流程是理解性能调优和搜索可见性最核心的部分之一。许多同学刚接触 ES 时,最常见的疑惑就是:“为什么我刚插入的数据查不到?”、“refresh 和 flush 有什么区别?”、“refresh_interval 设置成多少合适?”
这篇文章我们就专门讲清楚 refresh(刷新) 这一环节。它是 ES 写入流程的关键节点,既影响了数据什么时候能被搜索到,也影响了整个系统的写入性能和稳定性。
1. 什么是刷新时间
1.1 refresh 的定义
刷新 (refresh) = 把 内存 buffer 里的数据写到新的 segment 文件(先进入 OS cache),然后让它们对搜索可见。
刷新时间 (refresh interval) = ES 自动触发 refresh 的周期。
换句话说,refresh 的目标不是“数据持久化”,而是“数据可见”。也就是说,数据写进来之后,先保存在内存 buffer 里,这时候你去搜索是查不到的;一旦发生了 refresh,这些数据就会生成一个新的 Lucene segment,被索引打开,立即可搜索。
1.2 为什么需要 refresh?
Lucene 是一个基于 segment 的倒排索引系统。segment 文件是 只读的,所以每次有新文档进来,都要生成新的 segment。refresh 就是触发这个生成过程的机制。
如果没有 refresh,你写入的数据永远停留在 buffer 里,不会变成 segment,自然也就查不到。
2. 默认值与查询验证
默认情况下,ES 的 refresh_interval 是 1s。也就是说,ES 每秒会自动刷新一次,所以新写入的数据通常 1 秒内就能查到。
我们可以通过 _settings API 来查看:
1
2
# 查看某个索引的 refresh_interval
GET /my_index/_settings?include_defaults=true&filter_path=**.refresh_interval
返回结果示例:
1
2
3
4
5
6
7
8
9
{
"my_index": {
"settings": {
"index": {
"refresh_interval": "1s"
}
}
}
}
这说明 my_index 索引的刷新间隔是 1 秒。
如果你刚写入一条文档,立刻查询可能查不到,但只要等 1 秒钟,它就会出现在搜索结果里。这个“近实时(Near Real Time, NRT)”特性,就是 ES 的核心设计。
3. 修改刷新时间
在不同场景下,1 秒钟的 refresh_interval 并不是最优的。有时候我们希望更快可见,有时候则希望尽量少刷新,以提高写入性能。ES 允许你动态修改刷新间隔。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 设置为 30s(减少频繁刷新,写入性能更高)
PUT /my_index/_settings
{
"index": {
"refresh_interval": "30s"
}
}
# 禁用自动刷新(批量写入时常用)
PUT /my_index/_settings
{
"index": {
"refresh_interval": "-1"
}
}
设置为 30s:表示 30 秒刷新一次。适合日志类场景,写多查少,降低频繁小 segment 的生成。
设置为 -1:表示完全禁用自动 refresh,只能通过手动 _refresh 让数据可见。这个模式常见于 大批量数据导入。
4. 取值含义与场景适配
"1s":默认值,平衡写入和查询,适合绝大多数通用场景。
"30s" 或更大:适合日志/监控场景,写多查少,减少小 segment 生成,提高写入吞吐。
"-1":禁用自动刷新,通常用于大规模初始化导入数据。导入完成后,手动 refresh,再改回默认值。
5. 刷新时间对性能的影响
理解 refresh_interval 的性能影响非常重要。
时间越短(比如 500ms、1s)
优点:几乎实时可见,写入后很快就能查询到。
缺点:会生成大量小 segment,触发频繁的合并,降低整体写入吞吐。
时间越长(比如 30s、60s)
优点:减少 segment 数量,提升写入效率。
缺点:数据可见延迟更高。
禁用自动 refresh
优点:写入性能最佳,可以最大化导入速度。
缺点:完全不可查,必须手动 refresh 才能看到数据。
6. 实战建议
普通搜索型索引(电商商品、用户数据):用默认 1s。
日志/监控索引(写多查少):调大到 30s 或 60s,甚至更长。
大批量导入(初始化数据):设置 -1,导入后手动 refresh,再恢复 1s。
这种调优思路能够兼顾写入性能和搜索体验。
refresh_interval
数据可见性
写入性能
典型场景
1s (默认)
~1s 可查
中等
电商搜索、通用场景
30s
~30s 可查
较高
日志、监控
-1
手动可查
最高
大规模数据导入
7. refresh 参数取值详解
在写入 API 里,还可以通过 refresh 参数控制是否立刻刷新:
refresh=false(默认)
不会自动 refresh,性能最好。
新写的数据需要等下一个 refresh 周期才能查到。
refresh=true
执行完后强制 refresh。
数据立即可见,但每次都会触发 refresh,性能代价较大。
我们能够看到,即使 index_b 前面设置了”refresh_interval”: “-1”,再手动 refresh 之后也能够查找到了。
refresh=wait_for
不强制立即 refresh,而是等待下一个 refresh 周期完成后再返回。
适合既想要数据可见,又不想过多消耗性能的场景。
注意:如果索引设置了 "refresh_interval": "-1",那么 wait_for 会一直卡住不返回,这时候最好手动 refresh。
8. 手动刷新索引
在 Easysearch 里,手动刷新索引 就是调用 _refresh API。
这个操作会立刻把 内存 buffer 里的数据写到新的 segment(进入 OS cache),并让它们对搜索可见。
1
2
3
4
5
6
7
8
# 刷新单个索引
POST /my_index/_refresh
# 刷新多个索引
POST /index_a,index_b/_refresh
# 刷新整个集群
POST /_refresh
使用场景:
测试时:写入数据 → 立刻刷新 → 马上查。
批量导入时:禁用自动 refresh,导入完成后一次性手动 refresh。
⚠️ 注意:频繁手动 refresh 会导致大量小 segment,性能很差。生产环境中要谨慎使用。
9. 写入生命周期时间轴
为了更直观理解,我们看下 ES 写入生命周期:
1
2
3
4
5
6
时间轴 →
[写入] [refresh] [flush]
│ │ │
▼ ▼ ▼
文档写入 → Indexing Buffer → Segment(OS cache) → fsync磁盘
+ Translog(日志)
9.1 文档写入 (t=0)
文档进入 内存 buffer
同时写入 translog
此时数据不可查询。
9.2 refresh (t = refresh_interval)
buffer 转换为新的 segment 文件
segment 进入 OS cache
Lucene 打开 segment,数据可查询,但未必落盘。
9.3 flush
强制 fsync,把 OS cache 写到磁盘
清空 translog
此时数据既可查询,也保证持久化。
最佳实践流程:批量导入优化
新建索引 → 设置 refresh_interval=-1。
使用 _bulk 批量导入数据。
导入完成后,手动执行 POST /my_index/_refresh。
恢复 refresh_interval=1s(或业务需要的值)。
这样能显著提升导入性能,同时保证导入完成后数据立即可见。
📌 总结
refresh 决定了写入数据多久能被查询到。
它与 flush 不同:flush 是保证持久化,refresh 是保证可见性。
默认 1s,意味着 ES 是一个“近实时”系统。
在写多查少的场景,调大 refresh_interval 能显著提升性能。
在大规模导入时,禁用 refresh,然后手动 refresh,是常见的优化手法。
理解 refresh,不仅能帮助你解决“为什么数据查不到”的问题,还能让你在性能和实时性之间做出合理的权衡。
---
## 不用每次都改 `easysearch.yml` 也能改启动参数 —— 用 Docker 环境变量搞定一切
- URL: https://blog.no-claw.com/posts/ce1ebcd3/
- 日期: 2025-10-04
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
在用 Docker 部署 Easysearch 的时候,很多人习惯性地去改容器里的 easysearch.yml。
但每改一次,就得重建镜像或重新挂载配置,既不方便,也不利于自动化。
其实,Docker 天生就支持通过环境变量来传递参数。
只要我们把要改的配置写进 .env 文件,再用 --env-file 加载,就能在启动时覆盖 easysearch.yml 的对应设置。
这样,既不用改镜像,也不用动配置文件,还能方便地调试、切换和管理。
下面就来详细讲讲这套思路的原理、写法与实践。
Docker 环境变量机制简介
Docker 启动容器时,会把宿主机上的环境变量传递进容器内部。
容器里的程序(例如 Easysearch)在启动时,会读取这些变量并用来覆盖或替代默认配置。
简单来说:
1
环境变量 > easysearch.yml > 默认值
也就是说,只要我们在启动容器时提供了对应的环境变量,就能覆盖掉配置文件里的同名参数。
这就是“用 Docker 环境变量替代修改配置文件”的原理。
.env 文件写法
先准备一个 .env 文件(放在和 Docker 命令同级的目录下):
1
2
3
4
5
EASYSEARCH_INITIAL_ADMIN_PASSWORD=envfile123
cluster.name=mysearch
elasticsearch.api_compatibility=true
node.name=node-1
network.host=0.0.0.0
这几个变量的含义如下:
变量名
功能
说明
EASYSEARCH_INITIAL_ADMIN_PASSWORD
初始化管理员密码
推荐通过环境变量传递,安全又方便
cluster.name
集群名称
多节点部署时保持一致
elasticsearch.api_compatibility
是否启用 ES API 兼容模式
一般设为 true,方便客户端兼容
node.name
节点名称
区分不同节点
network.host
监听地址
0.0.0.0 表示允许外部访问
.env 文件的格式非常简单,每行一个 key=value,不要有多余空格,也不要加引号。
文件放在项目目录下即可,Docker 会自动读取。
启动容器:用 .env 文件注入配置
启动命令示例:
1
2
3
4
5
6
docker run -d \
--name easysearch \
--env-file ./.env \
-p 9200:9200 \
-p 9300:9300 \
easysearch:latest
这里的 --env-file ./.env 参数告诉 Docker 从 .env 文件中加载变量。
Docker 会自动把 .env 中定义的内容注入到容器环境中,EasySearch 启动时就会自动读取。
如果你想在启动时再临时改动一个参数,可以直接加 -e 选项:
1
2
3
4
5
6
7
docker run -d \
--name easysearch \
--env-file ./.env \
-e EASYSEARCH_INITIAL_ADMIN_PASSWORD=override123 \
-p 9200:9200 \
-p 9300:9300 \
easysearch:latest
这时候命令行里的 -e 会优先于 .env 文件的值。
验证环境变量是否生效
容器启动完成后,可以用 curl 验证 EasySearch 是否按 .env 中的配置运行。
1
curl -s -u admin:envfile123 http://localhost:9200
你会看到类似输出:
1
2
3
4
5
6
{
"name": "node-1",
"cluster_name": "mysearch",
"version": { "number": "8.13.0" },
"tagline": "You Know, for Search"
}
几个关键字段说明环境变量确实生效:
"name": "node-1" 来自 node.name
"cluster_name": "mysearch" 来自 cluster.name
管理员密码能登录,说明 EASYSEARCH_INITIAL_ADMIN_PASSWORD 已应用
查看容器内环境变量
如果想确认容器里到底有哪些环境变量,可以执行:
1
docker exec easysearch env
1
2
3
4
5
6
7
8
9
10
11
➜ 未命名文件夹 14 docker exec easysearch env
PATH=/sbin:/app/easysearch/jdk/bin:/app/easysearch/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=f3de6d6ab781
EASYSEARCH_INITIAL_ADMIN_PASSWORD=envfile123
cluster.name=mysearch
elasticsearch.api_compatibility=true
node.name=node-1
network.host=0.0.0.0
HOME=/root
或者只看我们关心的部分:
1
2
docker exec easysearch env | grep EASYSEARCH
docker exec easysearch env | grep cluster
这样能清楚看到 .env 文件中定义的变量是否真的被传进去了。
如果某个值缺失或拼写错误,这个方法很容易排查。
环境变量与配置文件的关系
在 Docker 镜像中,EasySearch 通常有一个默认的 easysearch.yml。
当容器启动时,程序会按以下优先级加载配置:
命令行参数或 -e 指定的环境变量
--env-file 传入的变量
容器内 /etc/easysearch/easysearch.yml 文件
内置默认值
因此,当你通过 .env 或 -e 设置参数后,这些值会覆盖配置文件里的同名项。
你完全不需要去修改容器内部的配置文件。
这正是现代容器化部署推荐的做法:
配置文件保持模板化,动态参数全部用环境变量注入。
实战建议
1. 保留 .env.example 模板
在项目目录中放一个 .env.example 文件,内容示例化:
1
2
3
4
5
EASYSEARCH_INITIAL_ADMIN_PASSWORD=changeme
cluster.name=my-cluster
elasticsearch.api_compatibility=true
node.name=node-1
network.host=0.0.0.0
其他成员部署时只需复制:
1
cp .env.example .env
再修改必要的值即可。
2. .env 不要进版本库
把 .env 加入 .gitignore,避免把真实密码上传。
3. 用不同 .env 文件区分环境
你可以创建多份环境文件:
1
2
3
.env.dev
.env.staging
.env.prod
启动时指定不同的文件:
1
2
docker run -d --env-file ./.env.dev ...
docker run -d --env-file ./.env.prod ...
这样一套镜像就能跑多个环境,彻底解耦配置与部署。
总结:环境变量让部署更轻、更灵活
通过 Docker 的环境变量机制,我们可以:
不再频繁修改 easysearch.yml;
用 .env 文件集中管理参数;
轻松区分不同环境;
无需重建镜像就能调整配置;
安全地注入密码等敏感信息。
从此以后,部署 EasySearch 只需要两步:
准备 .env;
一条 docker run --env-file 命令。
所有的参数都能即时生效,配置文件原封不动。
这就是现代容器化运维的思路:
“配置解耦、参数注入、环境即定义。”
当你下次用 curl 看见返回里显示的
cluster_name: mysearch、name: node-1,
那就是 .env 的功劳——
再也不用去翻 easysearch.yml。
---
## Docker 启动 Easysearch 时自定义初始密码的几种方式
- URL: https://blog.no-claw.com/posts/588b9d1d/
- 日期: 2025-10-04
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
在使用 Docker 部署 Easysearch 时,通常需要在启动容器时设置初始管理员密码。如果没有预先设置密码,系统可能会使用默认值或随机生成密码,不仅增加后续管理的复杂性,也存在安全隐患。
本文将详细介绍在 docker run 启动 Easysearch 容器时,通过不同方式传入环境变量(env)来自定义密码的多种方法。每种方法都配有实用示例和说明,帮助你根据实际环境灵活选择。
直接在命令中传入单个环境变量
这是最简单、最直接的方式,适合快速启动或临时测试场景。
1
2
3
4
5
docker run --name easysearch \
-e EASYSEARCH_INITIAL_ADMIN_PASSWORD=passwd123 \
--ulimit memlock=-1:-1 \
-p 9200:9200 \
infinilabs/easysearch:1.15.3
启动完成后,可进入容器验证变量是否生效:
1
2
docker exec -it easysearch bash
echo $EASYSEARCH_INITIAL_ADMIN_PASSWORD
输出结果:
1
passwd123
这个是在容器内部查看 ENV 的结果:
✅ 优点:
简单直接,适合手动调试;
不需要额外文件。
⚠️ 缺点:
密码会暴露在命令行历史中;
修改不便,不适合生产环境。
🗂 二、从环境文件加载(推荐)
如果你需要设置多个变量或希望更好地管理配置,建议使用 .env 文件。
这种方式清晰、安全、易于维护,是 最推荐的做法。
创建 .env 文件
在当前目录下新建 .env 文件:
1
EASYSEARCH_INITIAL_ADMIN_PASSWORD=envfile123
启动容器:
1
2
3
4
5
docker run --name easysearch \
--env-file .env \
--ulimit memlock=-1:-1 \
-p 9200:9200 \
infinilabs/easysearch:1.15.3
验证变量:
1
2
docker exec -it easysearch bash
echo $EASYSEARCH_INITIAL_ADMIN_PASSWORD
输出:
1
envfile123
✅ 优点:
环境变量集中管理;
避免明文密码出现在命令行;
文件可复用、便于协作。
⚠️ 注意事项:
.env 文件路径需正确(相对或绝对路径);
变量中含有空格或特殊字符时请用引号包裹:
1
EASYSEARCH_INITIAL_ADMIN_PASSWORD="P@ss word!123"
💡 补充说明:
在 Docker Compose 中,.env 文件会自动读取;
使用 docker run 时,必须显式指定 --env-file。
💻 三、引用宿主机环境变量
当宿主机上已经定义了某些变量时,也可以直接将它们传递给容器。
这种方式非常适合自动化脚本和 CI/CD 场景。
1️⃣ 导出宿主机变量
1
export EASYSEARCH_INITIAL_ADMIN_PASSWORD=hostenv123
2️⃣ 启动容器时引用
1
2
3
4
docker run -e EASYSEARCH_INITIAL_ADMIN_PASSWORD \
--ulimit memlock=-1:-1 \
-p 9200:9200 \
infinilabs/easysearch:1.15.3
或:
1
2
3
4
docker run --env EASYSEARCH_INITIAL_ADMIN_PASSWORD \
--ulimit memlock=-1:-1 \
-p 9200:9200 \
infinilabs/easysearch:1.15.3
验证变量:
1
2
docker exec -it easysearch bash
echo $EASYSEARCH_INITIAL_ADMIN_PASSWORD
输出:
1
hostenv123
✅ 优点:
更安全,不会在命令行中暴露;
适用于 CI/CD 环境和自动化部署。
⚠️ 注意:
启动容器前确认变量已导出;
使用 printenv 查看宿主机变量。
🔒 四、生产级方案:结合 Secret 管理工具
在生产环境中,建议不要直接在 .env 文件或命令行中存储密码。
可以结合以下工具来安全地管理敏感信息:
Docker Secrets:在 Swarm 模式中安全注入敏感值;
HashiCorp Vault / AWS Secrets Manager / Kubernetes Secrets:通过外部密钥服务动态传递;
**.gitignore**:确保 .env 文件不被提交到代码仓库。
⚙️ 五、验证环境变量是否生效
无论采用哪种方式,都可以使用以下命令验证:
1
docker exec -it easysearch env | grep EASYSEARCH_INITIAL_ADMIN_PASSWORD
或:
1
docker inspect easysearch | grep EASYSEARCH_INITIAL_ADMIN_PASSWORD
若输出包含你的密码值,即表示设置成功。
🧩 六、不同方式对比
方式
适用场景
安全性
便利性
是否推荐
命令行直接传入
快速测试
⭐
⭐⭐⭐
❌
.env 文件
开发/测试
⭐⭐
⭐⭐⭐⭐
✅
宿主机变量
自动化部署
⭐⭐⭐
⭐⭐⭐
✅
Secret 工具
生产环境
⭐⭐⭐⭐
⭐⭐
✅✅✅
📦 七、综合示例:多变量 + 默认值
假设我们要在生产环境部署,并定义多个参数:
.env 文件内容如下:
1
2
3
EASYSEARCH_INITIAL_ADMIN_PASSWORD=${EASYSEARCH_PASS:-default123}
EASYSEARCH_CLUSTER_NAME=prod_cluster
EASYSEARCH_HEAP_SIZE=4g
启动命令:
1
2
3
4
5
6
EASYSEARCH_PASS=SuperSecret123 \
docker run --name easysearch-prod \
--env-file .env \
--ulimit memlock=-1:-1 \
-p 9200:9200 \
infinilabs/easysearch:1.15.3
此时:
若外部定义了 EASYSEARCH_PASS,则使用该值;
否则回退到 .env 文件中的默认值 default123。
🧭 总结
在 Docker 环境中配置 Easysearch 初始密码的常用方式主要有三种:
命令行传入(-e):适合快速测试;
环境文件(–env-file):管理方便、最推荐;
引用宿主机变量:安全灵活,适合 CI/CD。
🚀 最佳实践建议:
开发环境使用 .env 文件;
生产环境使用 Secret 管理;
将 .env 文件加入 .gitignore;
定期轮换密码,确保安全。
参考命令:
1
2
3
4
5
docker run --name easysearch \
-e EASYSEARCH_INITIAL_ADMIN_PASSWORD=passwd123 \
--ulimit memlock=-1:-1 \
-p 9200:9200 \
infinilabs/easysearch:1.15.3
1
2
3
4
5
6
7
8
9
10
version: "3.3"
services:
easysearch:
restart: always
image: registry.lazycat.cloud/u04123229/infinilabs/easysearch:5dc6296f5370aacc
ports:
- 9200:9200
environment:
- EASYSEARCH_INITIAL_ADMIN_PASSWORD=admin123
networks: {}ß
---
## Elasticsearch 自动 Mapping 与 MySQL Schema 的对比分析
- URL: https://blog.no-claw.com/posts/ce1ebcd3/
- 日期: 2025-10-04
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
在企业级数据系统中,Elasticsearch(简称 ES) 与 MySQL 是两种完全不同的数据管理哲学。
一个是面向搜索与分析的分布式引擎,一个是面向事务与一致性的关系型数据库。
而当我们深入理解它们的数据结构定义方式——ES 的 自动 mapping 推断 与 MySQL 的 手动 schema 定义——就会发现,它们的核心设计理念几乎是两个世界。
本文将从机制、原理、优缺点和使用建议等角度,系统对比两者的差异,重点聚焦在 Elasticsearch 的自动 mapping 特性上。
一、什么是 Mapping 与 Schema
在 MySQL 中,我们习惯使用 表结构(Schema) 来定义数据字段及其类型:
1
2
3
4
5
6
CREATE TABLE user (
id INT PRIMARY KEY,
name VARCHAR(50),
age INT,
created_at DATETIME
);
每一行都必须严格遵守这个表结构,类型固定,字段不可缺少。
这是典型的 Schema-first 模型:在写入之前必须定义好结构。
而在 Elasticsearch 中,索引(Index)虽然也有 schema 概念,但它是通过 Mapping 来定义字段类型和分析方式的。
Mapping 可以手动声明,也可以让 ES 自动推断。例如:
1
2
POST /user/_doc
{ "name": "Alice", "age": 25 }
ES 会自动创建 user 索引,并根据字段值类型生成如下 mapping:
1
2
3
4
5
6
7
8
9
10
11
{
"mappings": {
"properties": {
"name": {
"type": "text",
"fields": { "keyword": { "type": "keyword" } }
},
"age": { "type": "long" }
}
}
}
这就是 自动 mapping(dynamic mapping):ES 在第一次看到某字段时自动推断类型并写入 mapping。
不需要提前定义结构,系统“自学习”出 schema。
二、自动 Mapping 的推断机制
Elasticsearch 的 mapping 推断是动态的(Dynamic Mapping)。当写入新文档时,ES 会:
扫描每个字段;
根据字段值的类型判断(string、number、boolean、date 等);
将结果写入 _mapping;
更新索引元数据并持久化。
例如:
值
推断类型
备注
"hello"
text + keyword
同时支持全文检索与精确匹配
123
long
数值型
12.3
double
浮点型
"2025-10-05"
date
自动识别日期格式
true
boolean
布尔值
ES 的这种自动识别让开发者在数据探索早期几乎零配置即可使用,非常便捷。
但这种便捷背后也隐藏着风险——错误推断、类型冲突、mapping 爆炸等问题可能在后期放大。
三、MySQL 的 Schema 定义机制
与 ES 不同,MySQL 属于 强类型、静态结构 模型。
它要求所有字段在写入前就被定义好。任何表结构变更都需要执行 ALTER TABLE,会产生锁表或重建索引的代价。
优点是:
数据一致性强;
结构清晰;
易于维护和优化;
支持事务与约束。
缺点则是:
演进成本高;
扩展不灵活;
对半结构化数据支持差。
如果说 MySQL 的 schema 是“一座刚性大厦”,那 ES 的 mapping 就像“可随时扩建的集装箱”。
四、自动 Mapping 的优势:灵活与速度
1. 开发效率高
在日志、埋点、IoT 等场景中,数据字段极多且经常变化。
自动 mapping 让开发者无需提前规划字段,只要把 JSON 写进去,ES 就能立刻索引和查询。
例如日志:
1
{ "host": "server-1", "response_time": 123, "status": 200 }
即使第二条日志多了新字段:
1
2
3
4
5
6
{
"host": "server-2",
"response_time": 98,
"status": 200,
"region": "ap-southeast-1"
}
ES 也会自动为 region 增加字段定义,无需手动修改 mapping。
这在 MySQL 中则必须执行结构变更。
2. 兼容性好
ES 的索引不要求所有文档字段一致。某些文档可以缺字段而不影响写入。
对动态 JSON 数据、日志、监控事件特别友好。
3. 适配性强
数据可来自多源系统(API、Kafka、日志流),字段差异大。
自动 mapping 让这些异构数据能快速进入索引,后期再统一分析。
4. 快速原型构建
在数据探索阶段,不必先定义 schema,就能立刻搜索和聚合,是数据科学家和分析工程师最喜欢的特性之一。
五、自动 Mapping 的风险与缺陷
1. 类型误判(Type Guessing)
ES 根据值内容推断类型,但不是总能猜对:
原始值
被推断类型
潜在问题
"00123"
text
实际上是字符串数字
"2024/12/01"
date
格式异常可能被误识别
123.0
double
实际希望 long,却被识别为浮点
true / false
boolean
可能来自字符串而非布尔值
类型一旦被推断并写入 mapping,就无法修改。
如果写错,只能重建索引并重新导入数据。
2. Mapping Explosion(映射爆炸)
ES 的每个字段都要占用堆内存(field data、倒排索引、统计信息)。
当系统存在动态命名字段(如 user_1, user_2, …)时,会生成成千上万个字段,导致:
Mapping 文件膨胀;
节点 heap 占用急剧上升;
查询性能下降;
甚至引发 “too many fields” 异常。
官方建议:单个索引字段数不要超过 1000。
3. 类型不可变
ES 中字段一旦创建,类型就锁定。
比如第一次写入 "price": "123" 被识别为 text,之后再写入数字 price: 123 就会报错:
1
mapper_parsing_exception: cannot merge a field of type [long] with [text]
修复方法只有一个:重建索引。
4. 搜索分析不准确
自动 mapping 对 string 默认生成 text + keyword 两种字段。
在全文检索时 OK,但在聚合、排序、精确匹配时会引发困惑。
许多用户在 Kibana 中查询 "region.keyword" 才能聚合,是由 mapping 自动生成机制决定的。
六、MySQL 的优势:可控与稳定
相比之下,MySQL 的 schema 固定、类型严格,所有字段定义都在 DBA 控制下。
这意味着:
任何类型变化都是显式的;
查询结果一致性强;
可维护性高;
性能优化空间大。
在交易、账务、库存、财务等场景中,MySQL 的稳定性远胜 ES。
它的缺点恰恰是 ES 的优点:灵活性差但可控。
七、如何平衡:让自动 mapping 可控
ES 提供了几种方式,在保留灵活性的同时减少风险。
1. 动态模板(Dynamic Templates)
你可以为自动推断加“模板规则”:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"dynamic_templates": [
{
"numbers": {
"match_mapping_type": "long",
"mapping": { "type": "float" }
}
},
{
"strings": {
"match": "*_id",
"mapping": { "type": "keyword" }
}
}
]
}
这样所有 _id 结尾的字段都被强制识别为 keyword,而不是 text。
2. 关闭自动 mapping
在生产环境常见做法是:
1
{ "dynamic": "strict" }
意味着:未定义字段禁止写入。
防止误导入数据结构。
3. 限制字段数
1
2
PUT /my_index/_settings
{ "index.mapping.total_fields.limit": 1000 }
避免 mapping explosion。
4. 明确控制核心字段
对关键字段(时间戳、数值、地理位置、ID)手动声明类型。
剩余部分交由 dynamic mapping 自动处理,可兼顾灵活与安全。
八、性能与存储影响对比
指标
Elasticsearch
MySQL
写入性能
较高(分布式、异步)
较低(事务同步)
读取性能
适合全文检索、聚合分析
适合主键查询、范围查询
schema 变更成本
无(自动)
高(需 ALTER)
内存消耗
较大(索引元数据)
较小
数据一致性
弱一致
强一致
横向扩展性
强
中等
结论:
ES 的自动 mapping 提供了极高的写入灵活性,但代价是索引元数据膨胀、内存占用高、类型错误风险大。
MySQL 的 schema 则更适合结构化、高一致性业务。
九、使用建议:什么时候用自动 mapping?
场景
建议
日志、监控、埋点
✅ 开启自动 mapping(dynamic=true)
搜索引擎、用户画像
✅ 自动 mapping + 动态模板
电商订单、金融账务
❌ 手动 mapping(dynamic=strict)
混合型数据(部分稳定、部分动态)
⚙️ 手动定义核心字段 + 动态模板控制扩展字段
MySQL → ES 同步
🚫 禁止自动 mapping,使用 Logstash/ETL 生成预定义 mapping
十、总结:灵活与秩序的取舍
维度
Elasticsearch 自动 Mapping
MySQL Schema
定义方式
自动推断
手动定义
灵活性
极高
低
一致性
弱
强
修改成本
低(但错误代价高)
高(但可控)
可维护性
中等(需监控 mapping 爆炸)
高
适用场景
搜索、日志、非结构化数据
交易、财务、结构化数据
Elasticsearch 的自动 mapping 是一把双刃剑:
它让数据“随写随用”,带来极大的灵活性;
但同时,也可能在规模化阶段埋下类型混乱、性能下降的隐患。
最佳实践是在项目早期利用其灵活性快速构建原型;
而在生产阶段,结合手动 mapping 与动态模板,建立“半自动、可控”的数据模型。
真正成熟的 ES 使用者,从来不会完全依赖自动 mapping。
自动化是起点,不是终点;灵活性需要以控制为前提。
---
## Easysearch 索引别名(Index Alias)详解
- URL: https://blog.no-claw.com/posts/c41993b0/
- 日期: 2025-10-04
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
在 Easysearch 中,索引别名(Index Alias) 是一种逻辑名称,它可以指向一个或多个真实索引。
使用别名的好处在于:
让应用层无需感知底层索引名变化;
方便进行索引切换、版本升级和数据迁移;
支持查询、写入、过滤、路由等控制;
实现读写分离或权限隔离。
简而言之,别名是索引的抽象层,就像数据库中的“视图(View)”或操作系统中的“符号链接(symlink)”。
创建索引别名
别名可以在创建索引时定义,也可以在已有索引上添加。
在创建索引时定义别名
1
2
3
4
5
6
PUT /logs_2025-10
{
"aliases": {
"logs_current": {}
}
}
该操作创建索引 logs_2025-10,并同时定义一个别名 logs_current。
之后,所有针对 logs_current 的查询都会路由到 logs_2025-10:
1
2
3
4
5
POST logs_2025-10/_doc
{"age":20}
GET /logs_current/_search
给现有索引添加别名
1
POST /logs_2025-10/_alias/logs_current_v2
或者使用 _aliases 批量操作:
1
2
3
4
5
6
POST /_aliases
{
"actions": [
{ "add": { "index": "logs_2025-10", "alias": "logs_current_v3" }}
]
}
查询与写入的区别
默认情况下,别名仅支持查询。
如果一个别名指向多个索引,那么写入(POST /alias/_doc)操作会报错。
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"error": {
"root_cause": [
{
"type": "illegal_argument_exception",
"reason": "no write index is defined for alias [logs_current_v2]. The write index may be explicitly disabled using is_write_index=false or the alias points to multiple indices without one being designated as a write index"
}
],
"type": "illegal_argument_exception",
"reason": "no write index is defined for alias [logs_current_v2]. The write index may be explicitly disabled using is_write_index=false or the alias points to multiple indices without one being designated as a write index"
},
"status": 400
}
为了解决这一问题,可以通过 is_write_index 参数指定某个索引作为写入目标。
1
2
3
4
5
6
7
POST /_aliases
{
"actions": [
{ "add": { "index": "logs_2025-10-01", "alias": "logs_all" }},
{ "add": { "index": "logs_2025-10-02", "alias": "logs_all", "is_write_index": true }}
]
}
此时:
查询 GET /logs_all/_search 会同时检索两个索引;
写入 POST /logs_all/_doc 时,数据会写入 logs_2025-10。
动态切换索引(零停机升级)
别名的最大优势之一是实现索引的无缝切换。
例如,应用程序始终通过 logs_all 查询数据,而底层实际索引会按天数变化。
切换示例:
1
2
3
4
5
6
7
POST /_aliases
{
"actions": [
{ "remove": { "index": "logs_2025-10-01", "alias": "logs_all" }},
{ "add": { "index": "logs_2025-10-03", "alias": "logs_all" }}
]
}
这里我移除了 logs_2025-10-01,然后添加了 logs_2025-10-03。
可以使用GET /_cat/aliases?v查看。
过滤别名(Filtered Alias)
别名还可以定义过滤条件,控制用户只能看到部分数据。
这是实现数据分区视图或权限隔离的常见方式。
它展示如何让一个别名只返回 region=china 的文档,而不暴露其他地区的数据。
1. 创建一个示例索引并插入数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PUT /transactions
{
"mappings": {
"properties": {
"region": { "type": "keyword" },
"user": { "type": "keyword" },
"amount": { "type": "float" }
}
}
}
POST /transactions/_bulk
{ "index": { "_id": 1 } }
{ "region": "china", "user": "alice", "amount": 100.0 }
{ "index": { "_id": 2 } }
{ "region": "usa", "user": "bob", "amount": 200.0 }
{ "index": { "_id": 3 } }
{ "region": "china", "user": "cindy", "amount": 150.0 }
{ "index": { "_id": 4 } }
{ "region": "japan", "user": "daisuke", "amount": 300.0 }
刷新索引:
1
POST /transactions/_refresh
创建过滤别名
定义一个只允许访问中国区数据的别名:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /_aliases
{
"actions": [
{
"add": {
"index": "transactions",
"alias": "transactions_cn",
"filter": {
"term": { "region": "china" }
}
}
}
]
}
使用过滤别名查询
1
GET /transactions_cn/_search
返回结果类似:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"hits": {
"hits": [
{
"_id": "1",
"_source": { "region": "china", "user": "alice", "amount": 100.0 }
},
{
"_id": "3",
"_source": { "region": "china", "user": "cindy", "amount": 150.0 }
}
]
}
}
可以看到:
来自 usa 和 japan 的记录不会出现在结果中;
别名层面自动做了过滤;
应用层调用时完全不需要在查询语句中加 term 条件。
路由别名(Routing Alias)
Elasticsearch 的数据分片(sharding)是通过一个公式决定的:
1
shard = hash(routing) % number_of_primary_shards
默认情况下,routing = _id
但如果你有多租户、分国家、分部门的场景,可以用业务逻辑字段当作 routing。
routing 相同的数据会落在同一个分片上,提高写入和查询的性能。
因此:
把别名和 routing 绑定起来,可以实现“逻辑分区 + 性能优化 + 查询隔离”。
下面通过一个完整的数据例子演示。
创建索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
PUT users
{
"settings": {
"number_of_shards": 4,
"number_of_replicas": 0
},
"mappings": {
"properties": {
"name": { "type": "keyword" },
"country": { "type": "keyword" },
"age": { "type": "integer" }
}
}
}
创建带 routing 的别名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /_aliases
{
"actions": [
{
"add": {
"index": "users",
"alias": "users_cn",
"routing": "china"
}
},
{
"add": {
"index": "users",
"alias": "users_us",
"routing": "usa"
}
}
]
}
✅ 我们现在有两个逻辑视图:
Alias
Routing
用途
users_cn
"china"
代表中国用户
users_us
"usa"
代表美国用户
通过别名写入数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
POST users_cn/_doc
{
"name": "张伟",
"country": "CN",
"age": 29
}
POST users_cn/_doc
{
"name": "王芳",
"country": "CN",
"age": 34
}
POST users_us/_doc
{
"name": "John",
"country": "US",
"age": 42
}
POST users_us/_doc
{
"name": "Emily",
"country": "US",
"age": 31
}
✅ 实际都写入到同一个物理索引 users,
但数据被根据 routing(china / usa)分到了不同分片。
4️⃣ 验证分片路由情况
1
GET users/_search_shards
返回示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
{
"nodes": {
"mIS34pJJRrWWYDERAJLuqw": {
"name": "node-1",
"ephemeral_id": "BgJLaTndTAWGxDGri8125w",
"transport_address": "172.100.1.2:9300",
"attributes": {}
}
},
"indices": {
"users": {}
},
"shards": [
[
{
"state": "STARTED",
"primary": true,
"node": "mIS34pJJRrWWYDERAJLuqw",
"relocating_node": null,
"shard": 0,
"index": "users",
"allocation_id": {
"id": "XT6Ds-NTSb-hhUeNCeHHjA"
}
}
],
[
{
"state": "STARTED",
"primary": true,
"node": "mIS34pJJRrWWYDERAJLuqw",
"relocating_node": null,
"shard": 1,
"index": "users",
"allocation_id": {
"id": "74BEgGYOTOO0tg7kVXtvDA"
}
}
],
[
{
"state": "STARTED",
"primary": true,
"node": "mIS34pJJRrWWYDERAJLuqw",
"relocating_node": null,
"shard": 2,
"index": "users",
"allocation_id": {
"id": "9hdcuoz5TbWql3kipudCxA"
}
}
],
[
{
"state": "STARTED",
"primary": true,
"node": "mIS34pJJRrWWYDERAJLuqw",
"relocating_node": null,
"shard": 3,
"index": "users",
"allocation_id": {
"id": "h0esaYy8QJmvfiGLjn3Zwg"
}
}
]
]
}
查询数据
查询中国区用户:
1
GET users_cn/_search
输出:
1
2
3
4
5
6
7
8
9
{
"hits": {
"total": 2,
"hits": [
{ "_source": { "name": "张伟", "age": 29 } },
{ "_source": { "name": "王芳", "age": 34 } }
]
}
}
查询美国区用户:
1
GET users_us/_search
输出:
1
2
3
4
5
6
7
8
9
{
"hits": {
"total": 2,
"hits": [
{ "_source": { "name": "John", "age": 42 } },
{ "_source": { "name": "Emily", "age": 31 } }
]
}
}
如果直接查物理索引
1
GET users/_search
返回所有 4 条记录,因为没带 routing。
再加一个过滤型 alias(可选)
1
2
3
4
5
6
7
8
9
10
11
12
POST /_aliases
{
"actions": [
{
"add": {
"index": "users",
"alias": "users_adults",
"filter": { "range": { "age": { "gte": 30 } } }
}
}
]
}
然后查询:
1
GET users_adults/_search
→ 只返回 王芳(34 岁)和 John(42 岁)和 Emily(31 岁)。
。
场景
routing 带来的好处
写入
相同 routing 的文档总是写入同一分片,减少 shard 跳转
查询
查询时只访问一个 shard,速度可提升数倍
多租户
每个租户 routing 不同,实现物理隔离
地域分区
中国区、美国区等逻辑分区共享同一个索引
查看与删除别名
查看当前集群中所有别名:
1
GET /_cat/aliases?v
输出结果:
1
2
3
alias index filter routing.index routing.search is_write_index
logs_current logs_2025-10 - - - -
logs_all logs_2025-10 - - - true
删除别名:
1
DELETE /logs_2025-10/_alias/logs_current
或:
1
2
3
4
5
6
POST /_aliases
{
"actions": [
{ "remove": { "index": "logs_2025-10", "alias": "logs_current" }}
]
}
总结
Easysearch 的索引别名是一个轻量、强大且几乎“零成本”的机制,它在索引生命周期管理中起着核心作用。
合理使用别名,可以实现:
热切换(零停机索引迁移);
分片控制(按租户或地理位置隔离);
安全访问(按条件过滤可见数据);
持续演进(读写分离 + 版本平滑过渡)。
对于任何生产环境的 Easysearch 集群来说,别名是不可或缺的基础能力。
---
## 懒猫微服炫技篇(四):蓝牙音浪,懒猫开唱
- URL: https://blog.no-claw.com/posts/afaab095/
- 日期: 2025-10-02
- 分类: 懒猫微服 / 炫技
- 标签: 懒猫微服
懒猫微服硬件层面自带了蓝牙模块。理论上,它应该可以像一台笔记本电脑或手机那样,直接连接蓝牙音箱,成为一个音频输出端。那么问题来了:在没有桌面 GUI 的前提下,能否纯命令行完成 蓝牙配对 + 音乐播放?
折腾了一上午,我从安装依赖、加载服务、设备配对到最终的音频播放,完整走通了流程。过程中踩了几个典型的坑,把命令总结下来,甚至进一步改造成一个家庭“蓝牙音频中心”。
准备工作
懒猫微服本质上就是一台 Linux 设备,可以在 neofetch 中查看系统信息。所以蓝牙相关的配置流程和常见的 Ubuntu/Debian 系统基本一致。只要我们搞定了蓝牙协议栈、音频服务和配对,就能让它像 PC 一样推送音频到蓝牙音箱。
懒猫微服没有提供 GUI 桌面环境,所以所有的这些步骤都得通过命令行完成。
安装依赖
首先安装系统缺少的几个依赖包:
1
sudo apt install bluez bluez-tools pulseaudio-module-bluetooth libspa-0.2-bluetooth
这里有几个关键点:
bluez:Linux 的蓝牙协议栈,底层必须有这个,否则系统完全不认识蓝牙设备。
bluez-tools:命令行工具集合,比如 bluetoothctl 就靠它。
pulseaudio-module-bluetooth:PulseAudio 的蓝牙扩展模块,用于把音频流通过 A2DP 推给蓝牙音箱。
libspa-0.2-bluetooth:PipeWire 的蓝牙插件,懒猫微服上装着可能是新版本的音频栈,有时候需要它做兼容。
重启蓝牙服务
1
sudo systemctl restart bluetooth
蓝牙服务在驱动加载、硬件唤醒等场景下,偶尔会出现异常,导致设备不可见。重启服务能有效避免 无法扫描设备、连接超时 等问题。
如果遇到 No default controller available 的错误,可以通过 systemctl status bluetooth 检查蓝牙守护进程是否正常运行。
蓝牙配对流程
进入蓝牙控制台:
1
bluetoothctl
核心工具是 bluetoothctl。进入交互模式后,依次执行:
1
2
3
4
5
6
7
power on
agent on
default-agent
scan on
pair
trust
connect
逐条解释一下:
power on:打开蓝牙硬件。
agent on + default-agent:启用配对代理,否则你输入 pair 时可能会报 “No agent available”。
scan on:扫描附近设备,你会看到形如 XX:XX:XX:XX:XX:XX 的 MAC 地址和设备名。
pair :和音箱配对。第一次一般要确认一下。
trust :把设备设为可信,下次开机会自动连接。
connect :连接上去,成功的话会提示 Connection successful。
我使用的是纽曼的蓝牙音箱,蓝牙控制台的输出大概是这样的:
当出现“成功配对并已连接”时,说明音频链路已建立。
播放音乐
安装轻量级播放器:
1
sudo apt install mpg123
执行播放:
1
mpg123 hello_tuya.mp3
运行时终端显示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(base) lzcbox-029c588e ~ # mpg123 hello_tuya.mp3
High Performance MPEG 1.0/2.0/2.5 Audio Player for Layers 1, 2 and 3
version 1.31.2; written and copyright by Michael Hipp and others
free software (LGPL) without any warranty but with best wishes
Terminal control enabled, press 'h' for listing of keys and functions.
Playing MPEG stream 1 of 1: hello_tuya.mp3 ...
> 01+97 00:00.00+00:03.45 --- 100=100 8 kb/s 36 B acc 0 clip p+0.000
MPEG 2.0 L III vbr 16000 mono
[0:03] Decoding of hello_tuya.mp3 finished.
这时候音箱就能播放我们加载的 MP3 文件了。
总结流程(最小命令集)
最终可以整理成如下最小命令集:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1. 安装依赖
sudo apt install -y bluez bluez-tools pulseaudio-module-bluetooth libspa-0.2-bluetooth
# 2. 重启蓝牙服务
sudo systemctl restart bluetooth
# 3. 进入蓝牙控制台做配对
bluetoothctl
# power on
# agent on
# default-agent
# scan on
# pair
# trust
# connect
# 4. 测试播放
sudo apt install -y mpg123
mpg123 hello_tuya.mp3
踩坑与经验分享
连接失败(br-connection-page-timeout)
有时候会报类似:
1
Failed to connect: org.bluez.Error.Failed br-connection-page-timeout
这个大概率是音箱没进入配对模式,或者离设备太远。解决方法就是:重置音箱 → 靠近设备 → 再连。
开机自启
懒猫微服重启后可能蓝牙 sink 不会自动挂载,你需要写个 systemd 服务,启动时执行 bluetoothctl connect 。
容器化方案
如果想更整洁,可以拉个容器专门跑音乐服务,把 mp3 拉进去播放,或者跑个 MPD(Music Player Daemon)+ ncmpcpp。这样甚至可以做一个“云端点歌机”。
其实做到这一步后,懒猫微服就具备了一个“音频推送中转站”的能力。未来可以玩出很多花样:
自动播放提醒:结合 crontab,在每天早上 8 点自动播一首歌当闹钟。
语音播报:结合 TTS(文字转语音),让懒猫微服播报天气、消息、提醒。
网络收音机:用 mpg123 http://... 直接播放网络电台。
家庭中控:接入 Home Assistant,把蓝牙音箱当智能家居设备控制。
写在最后
懒猫微服虽然定位是轻量 NAS/微服务平台,但它的硬件能力其实远比我们想象的要强。今天这一折腾,我把它变成了一个“小型蓝牙音频播放器”。这种玩法对技术宅来说挺有意思:你既能学到 Linux 蓝牙栈和音频服务的原理,又能把手头的小盒子改造成一个实用的设备。
---
## 懒猫微服炫技篇(九十九):从今天起,不用安装客户端也能访问懒猫微服啦~
- URL: https://blog.no-claw.com/posts/cc9f9be0/
- 日期: 2025-09-30
- 分类: 懒猫微服 / 炫技
- 标签: 懒猫微服
在日常使用懒猫微服的过程中,我经常会遇到这样一种情况:
当我想把网盘里的内容分享给朋友时,发现对方需要先下载懒猫微服的 APP,然后还要注册、登录,整个流程相对繁琐。对于大多数电脑小白来说,这一关可能直接劝退。如果对方只是临时想要下载一个文件,比如一个 PDF 文档或者一张照片,这样的门槛显得过高,不太符合“即开即用”的理念。
那么,有没有可能在不安装客户端的情况下,也能直接通过浏览器访问到懒猫微服的内容?
这篇文章就是我折腾出来的一个解决方案:通过代理的方式,把原本只能在客户端访问的资源,转化为“浏览器可直达”的体验。下面我会一步步展开。
灵感的由来
这个灵感并不是凭空出现的,而是源自我之前的工作经历。那时,公司网络屏蔽了维基百科,导致很多查资料的场景非常不方便。解决方案就是通过配置特定的 HTTP 代理,把流量转发出去,从而实现访问被封锁的网站。
在那个时期,我经常需要和公司的 IT 的同事沟通这些事情。这段经历让我意识到,代理其实就是一种“流量搬运工”。它不需要改变服务本身,而是通过“转发”让原本不可达的资源变得可达。
于是我就想:能不能借助代理,把访问懒猫微服的流量“搬运”出来,从而绕过必须安装客户端的限制?
代理的基本思路
我们可以把这个问题抽象成一个三方交互的模型:
A:客户端设备(比如我的 iPad)
B:代理服务器(也就是我在懒猫微服上部署的另一台 PC 或者路由器)
C:目标网站(也就是懒猫微服的 Web 服务入口)
通常情况下,A 是无法直接访问 C 的,因为懒猫微服要求使用它的 APP 来做身份校验。
但如果我在 B 上部署一个代理,让 A 的请求先走 B,再由 B 转发到 C,那么 A 就能“曲线救国”。
这种思路其实就是“中转站”模型:
对 A 来说:它只需要知道怎么连到 B,并不关心后面发生了什么。
对 C 来说:它只知道自己收到了请求,但并不清楚请求最初来自哪里。
这就是代理的核心魅力:用最小的改动,让本来不兼容的系统实现互通。
两种可行的代理方式
我主要尝试了两种代理思路,每一种都有各自的适用场景:
1. 透明代理
透明代理的原理是:把客户端的默认网关改成代理服务器的地址,让所有流量都先经过代理,然后再由代理进行转发。
优点:
客户端无感知,不需要手动配置浏览器或应用里的代理。
适合家庭或小型局域网环境,一次配置后所有设备都能享受便利。
缺点:
需要控制 DHCP 或手动改网关,对普通用户来说稍微复杂。
如果局域网环境比较复杂,可能会出现路由冲突或 IP 配置错误。
2. HTTP 代理
HTTP 代理则更直观:直接在客户端的网络设置里,配置一个 HTTP 代理(IP + 端口),所有浏览器流量就会走这个代理。
优点:
操作简单,任何支持代理设置的设备都能快速配置。
灵活性高,可以按需开关。
缺点:
某些应用不会遵守 HTTP 代理设置,比如一些直连的客户端或 P2P 软件。
对于非 HTTP 协议的流量支持有限。
实际操作:iPad 接入代理
我最终选择了 HTTP 代理方案。操作过程很简单:
在登陆懒猫微服的一个电脑上上部署了一个 HTTP 代理。
在 iPad 的 WiFi 设置里,填上代理服务器的地址和端口。
保存后重新打开浏览器,直接访问 heiyu.space 域名。
结果很惊喜:在没有代理之前,这个域名是打不开的;在配置了代理之后,页面顺利加载了。
这意味着,即便没有安装懒猫微服 APP,也能直接通过浏览器访问资源。
如果采用透明代理,操作方式是:在 iPad 的网络设置里,把默认网关改为代理机器的内网 IP。实际效果展示
配置前
从截图中可以看到我本地没有安装猫微服客户端,如果不配置网关的这种情况回无法访问 heiyu.space,浏览器报错。
配置后
配置好跳板机之后,懒猫微服的启动器页面顺利加载。
甚至可以进到我的面食比例计算机,里面的内容直接用浏览器访问。
适用的场景
这种方案在日常生活中非常有用:
临时分享文件
想让朋友下载一个大文件,但又不想让他专门注册一个懒猫微服账号。通过代理,朋友只需要浏览器就能搞定。
跨平台访问
有些设备(智能电视、游戏机、打印机)无法安装懒猫微服客户端,但仍然可能需要访问内容,这时代理就是最优解。
应急访问
出差在外,临时需要访问家里的 NAS 或懒猫微服,但设备条件有限。只要提前部署好代理,就能轻松接入。
局域网共享
家里有多台设备想同时访问懒猫微服,但不想每台都安装客户端。直接通过透明代理,全屋共享。
总结
以前分享文件给朋友,还是觉得流程太重:下载 APP、注册账号、登录,再操作一堆。现在,我只要这样一个流量转发,就能把分享变成真正的一件小事——点开链接,直接下载。
这才是我想要的体验:技术改变生活。
---
## 在嘉立创的泰山派上也能运行Easysearch
- URL: https://blog.no-claw.com/posts/d0139002/
- 日期: 2025-09-30
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
最近一段时间我折腾硬件比较多,经常翻箱倒柜找各种开发板出来玩。某天在角落里翻到一块嘉立创的泰山派开发板(Taishan Pi),这是一块基于 Rockchip RK3566 的嵌入式 Linux 板卡。严格来说,它的性能比树莓派还要逊色一些,尤其是 CPU 主频和内存带宽方面。但手痒之下,我突然想到了一个念头:能不能在这样一块嵌入式开发板上跑一个完整的 Easysearch 实例呢?
Easysearch 本质上是一个搜索引擎数据库,是 Elasticsearch 的国产化替代方案。它在大多数情况下被部署在 x86_64/arm 架构的服务器上,搭配 SSD 或 NVMe 作为存储,用来做全文检索、大规模日志分析或向量搜索。在常规的生产场景中,我们很少会把它和“嵌入式开发板”联想在一起。毕竟,后者 CPU 性能有限、内存紧张、存储设备大多是 eMMC 或低速 SD 卡,看起来完全不是数据库的适配环境。
不过,学习和实验的环境往往不需要极致的性能。于是,我决定尝试一下,把 Easysearch 移植到泰山派上跑起来。
导出镜像
由于我的开发板连接 Dockerhub 经常超时, 而我的 MacOS 有之前缓存的 Docker 镜像。我的第一步是把镜像在 Mac 上先导出。
1
docker save -o easysearch-1.15.0.tar infinilabs/easysearch:1.15.0
上面这条命令会把名为 infinilabs/easysearch:1.15.0 的镜像打包成一个 tar 文件,文件大小大概 700MB 左右。导出的好处是:我不需要在嵌入式开发板上重新去拉取 Docker Hub 镜像(速度慢且容易失败),只需要把 tar 包通过 FTP 或者 SFTP 上传过去即可。
上传到开发板
由于泰山派跑的是一个裁剪过的 Linux 系统,自带 SSH 和 FTP 服务,所以我直接用 FTP 客户端把 easysearch-1.15.0.tar 上传到板子的 /home 目录。上传速度受限于板载 eMMC,大概 3MB/s 左右,耐心等一会儿就好。
上传完毕后,我用 SSH 登录到开发板。
加载镜像
进入板子后,第一件事情就是把镜像导入到本地 Docker:
1
docker load -i easysearch-1.15.0.tar
等待几分钟,Docker 会把镜像重新解压并注册到本地。由于板子 CPU 性能有限,这个步骤比在笔记本上要慢不少,但最终会看到 familiar 的镜像 ID 出现在 docker images 的列表中。
调整内核参数
接下来要注意的就是一个经典坑:Easysearch/Elasticsearch 类的数据库要求内核参数 vm.max_map_count 至少设置为 262144,否则启动会直接报错。
1
sudo sysctl -w vm.max_map_count=262144
这条命令会临时修改内核参数。如果你打算长期使用,可以把它写到 /etc/sysctl.conf 或者 /etc/sysctl.d/ 里。
启动容器
有了镜像和内核参数,接下来就是熟悉的 docker run 了:
1
2
3
4
docker run --name easysearch \
--ulimit memlock=-1:-1 \
-p 9200:9200 \
infinilabs/easysearch:1.15.3
第一次启动时,容器会打印出初始的超级管理员密码,可以在日志里找到。
UI 访问与体验
容器启动后,在浏览器访问开发板的 IP 地址的 9200 端口,就能打开 Easysearch 的自带管理 UI。
UI 上显示,除了内存很快被吃满之外,其他方面表现完全可用。毕竟 Easysearch 默认会尝试尽可能缓存索引数据,而 RK3566 开发板通常只有 2GB 或 4GB 内存,溢出是难免的。但从查询速度和索引写入的小规模实验来看,性能并没有想象中那么糟糕。我尝试运行了一些 DSL,也没有什么问题。
换句话说,虽然这套组合是 arm CPU + eMMC 存储,但作为学习和体验之用已经足够。
性能观察
为了更直观地观察运行情况,我在容器启动后打开 htop。
可以看到 Easysearch 启动初始化时 CPU 占用飙升,内存使用量也接近物理上限。但在完成索引加载后,CPU 占用下降明显,保持在个位数到十几的水平。
这说明在嵌入式环境下跑数据库的瓶颈更多是 初始化 和 大规模数据处理,而不是日常的小规模查询。对于个人实验、API 测试和功能熟悉,完全可以胜任。
适用场景与局限
把 Easysearch 部署在 RK3566 这样的小板子上,意义主要有两个:
学习和实验环境:如果你只是想熟悉 Easysearch 的 API、UI 界面,或者学习 Elasticsearch 生态,不必为了一点点测试数据开一台大服务器。
轻量级应用:某些嵌入式场景下需要本地搜索功能,比如日志收集、离线数据查询、小型 IoT 网关,Easysearch 也能派上用场。
当然,它也有明显的局限:
内存限制:2GB 内存基本无法支撑大规模索引,超过几十万文档就会吃紧。
存储性能:eMMC 的顺序写速度大约 40MB/s,随机写更低,这对索引写入速度有一定影响。
CPU 性能:四核 ARM Cortex-A55,单核性能有限,并发查询时表现会受限。
总结
这次尝试算是一次“异想天开”的实验:把 Easysearch 从 PC 搬到了一块嵌入式开发板上。最终结果是——它真的能跑,而且还比预想的顺畅。除了内存不足和初始化速度慢,实际使用体验完全能满足学习场景。
如果你平时不想让笔记本长时间开着数据库,又恰好手边有一块开发板,那么完全可以用类似的办法,把 Easysearch 跑在开发板上作为一台“轻量数据库机”。
一句话总结: Easysearch 其实没有你想象中那么“重”,只要底层 Linux 和 Docker 能跑,它就能跑。
---
## 懒猫算力仓初探(一):开箱手记
- URL: https://blog.no-claw.com/posts/7ae44bfb/
- 日期: 2025-09-25
- 分类: 懒猫微服 / 算力仓
- 标签: 懒猫微服, NAS
懒猫微服最近上新了一款产品,名字叫 「懒猫 AI 算力仓」。
作为老用户,首发自然第一时间支持了一下。
其实我对这个设备的期待已经很久了。过去一年里,AI 大模型的爆发几乎让每个技术人都产生过同一个念头:能不能有一台属于自己的“算力仓”?不用再排队租云 GPU,不用担心账单像无底洞一样增长,更不用把敏感数据传到云端。所以赶了个首发,等了两天拿到了商品。
懒猫 AI 算力仓的核心是 NVIDIA Jetson AGX Orin。
这块板子可能很多人都听过,它本来的定位是“边缘计算”和“机器人中枢”,算是英伟达给嵌入式 AI 场景设计的亲儿子产品。比如跑 YOLO 目标检测、机械臂控制,这些都是它的常见场景。
除此之外, Orin 还有一个优势,就是 完整继承 CUDA 生态。这就意味着,它和桌面 GPU 一样,能跑主流大模型和 AI 框架。相比之下,很多国产芯片虽然跑分也许很高,但因为缺乏 CUDA 生态,真正落地的时候往往要做大量适配工作。可能你下载了模型,但能不能跑起来就是另一个问题了。
CUDA = 生态优势。
这点在 AI 时代非常关键。使用懒猫 AI 算力仓,下载完模型就能马上测试,而非 CUDA 芯片用户,可能还要花上几个月适配环境。这就是差距。
所以当懒猫把 Orin 改造成家用级的 AI 算力仓时,我的第一反应就是:这玩意儿正好补上了家里缺的那块“私有超算”拼图。
包装比我预想的更精美,拆开的时候甚至有点“科幻感”。外观是简洁的机身,棱角分明,风格上有点像星球大战里的装置。
尽管实物拿着沉甸甸的,但尺寸比我想象的还要小一些,高度大约只有懒猫微服的 NAS 一半。拿在手里的重量也不到 1300 克,比 MacBook Pro 还要轻很多,所以理论上你带出门也不是不行(不是)。
这种“袖珍 + 全固态”的组合,第一观感就是结实耐用,而且空间利用率极高。塞进机柜里刚刚好。
接下来看看核心参数:
GPU:275T 算力,64 Tensor Core,2048 核心,最高 1.3GHz
CPU:12 核 ARM Cortex-A78AE,最高 2.0GHz,轻量推理和任务调度非常合适
显存:LPDDR5,带宽 204.8GB/s,容量高达 64GB(显存+内存一体化设计)
这套组合,基本就决定了它能直接运行 70B 参数级别的大模型,而且是真正意义上的“无限 Tokens”,不像云端那样有额度限制。
在存储扩展方面:
自带 64GB eMMC 5.1 系统盘(足够跑系统和基础应用)
预留 2 个 M.2 插槽,最多支持 32TB SSD,这就意味着即便你想同时存储多个大模型,也能一次性装下。
接口配置也很周到:
视频输出:HDMI
外设:2 个 USB 3.2
网络:双网口(10G + 2.5G),特别适合多机联动或做本地集群
额外:还支持 WiFi6,无线连接没问题
官方参数写着整机功耗 64W,所以很省电。要知道,一块台式机的 4090 往往就能拉到 400W,而 Orin 的 64W 几乎可以说是“节能怪兽”。我把一个小型工作站,直接塞进了我的机柜里。
这次买算力仓,还有一个重要原因:我家里已经在用懒猫微服的生态。算力仓一接入,就能和原来的设备形成闭环。
比如:
NAS 负责存储数据和模型;
算力仓负责推理和生成;
懒猫微服的软件层负责调度和调用。
这种组合让我感觉像是给自己搭了一个“家用 AI 实验室”。来一张和机柜里其他设备的合影 ↓
懒猫算力仓的应用场景非常多:
大模型推理:本地跑 70B 模型,直接当私有 ChatGPT,避免云端隐私风险。
文生图 / 文生视频:跑 Stable Diffusion、Luma AI 之类的模型,图生视频速度应该会很快。
科研实验:对于学生和研究者来说,算力仓相当于一台低功耗的工作站,可以在寝室里跑实验。
企业内部 POC:小公司如果要测试 AI 原型,直接上算力仓,比租云 GPU 灵活得多。
边缘推理 + 本地 RAG:结合家里的数据仓库,跑一个完全离线的知识问答系统,做“自己的 AI 知识助手”。
整体来说,懒猫 AI 算力仓给我的感觉就是:
小小一台,参数硬核;
CUDA 生态无缝兼容,开箱即跑大模型;
扩展灵活,功耗低,噪音小;
和懒猫微服生态结合,几乎是“家用 AI 实验室”的标配;
对我这种经常要跑 RAG、文生图的场景非常友好。
如果说之前还在犹豫“要不要上云 GPU”,现在真的可以考虑直接在家里搞一台 Orin 算力仓。算力、省钱、数据安全,全部兼顾。
更重要的是,这不是一台冰冷的机器,而是让我随时能打开 AI 世界大门的“钥匙”。
---
## 懒猫算力仓初探(三):如何 SSH 免密登陆以及配置免密 sudo?
- URL: https://blog.no-claw.com/posts/a4c04b60/
- 日期: 2025-09-25
- 分类: 懒猫微服 / 算力仓
- 标签: 懒猫微服, NAS
在拿到懒猫 AI 算力仓之后,除了插显示器操作,更常见的方式是通过 SSH 远程管理。这样不仅方便日常使用,也能在不接显示器的情况下完成调试和部署。本文将一步步演示如何用 SSH 登录算力仓,并配置免密登录与免密 sudo,让远程操作更高效、更顺滑。
懒猫算力仓启动后,我们可以通过 SSH 来进行远程管理。设备出厂时已默认开启 SSH 服务端,所以我们要做的第一步是获取它的 IP 地址。
一、获取 IP 地址
如果你使用路由器,可以在路由器后台查看分配到的 IP,并用 telnet ip地址 测试端口是否开放:
如果外接了显示器,也可以直接在终端执行:
1
ip addr
懒猫算力仓的默认用户名和密码都是 nvidia,所以我们可以直接尝试登录:
1
ssh nvidia@192.168.1.100
首次登录会提示是否接受 fingerprint,输入 yes 即可。
二、配置 SSH 免密登录
每次输入密码太麻烦,可以配置免密登录。
在本地生成 SSH 密钥(如果还没有):
1
ssh-keygen -t rsa -b 4096
一路回车,默认生成在 ~/.ssh/id_rsa 和 ~/.ssh/id_rsa.pub。
将公钥拷贝到目标机:
1
ssh-copy-id nvidia@192.168.1.100
如果目标机没有 ssh-copy-id,可以手动追加:
1
cat ~/.ssh/id_rsa.pub | ssh nvidia@192.168.1.100 "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
以后只需执行:
1
ssh nvidia@192.168.1.100
就能直接登录而无需密码。
三、使用 ~/.ssh/config 简化命令
如果你经常要连同一台设备,可以在本机配置 SSH 简化命令。
编辑:
1
vim ~/.ssh/config
添加配置:
1
2
3
4
5
Host orin
HostName 192.168.1.100
User nvidia
Port 22
IdentityFile ~/.ssh/id_rsa
保存后,就可以直接:
1
ssh orin
配合免密登录,体验更流畅。
四、配置免密 sudo
Ubuntu 上免密 sudo 有两种方式:
方式一:直接修改 sudoers 文件
进入算力仓:
1
sudo vim /etc/sudoers
找到:
1
root ALL=(ALL:ALL) ALL
在下面新增:
1
nvidia ALL=(ALL:ALL) NOPASSWD:ALL
保存退出即可。
方式二(推荐):新建 sudoers.d 配置文件
这样不会污染原始配置,更安全:
1
2
echo "nvidia ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/nvidia
sudo chmod 440 /etc/sudoers.d/nvidia
五、验证配置
尝试免密 SSH 登录。
登录后执行:
1
sudo ls /root
如果无需输入密码,说明免密 sudo 已生效。
六、SSH 常见排查方法(通用 Linux)
如果 SSH 无法登录,可以按“本地 → 网络 → 服务端 → 配置”逐步排查:
1. 确认基本信息
用户名、IP 是否正确:
1
ssh user@192.168.1.100
如果端口改过,需显式指定:
1
ssh -p 2222 user@192.168.1.100
2. 本地网络检查
测试连通性:
1
ping 192.168.1.100
确认端口是否开放:
1
2
3
telnet 192.168.1.100 22
# 或
nc -zv 192.168.1.100 22
3. 服务端检查
确认 SSH 服务是否运行:
1
2
sudo systemctl status ssh # Ubuntu/Debian
sudo systemctl status sshd # CentOS/RedHat
若未运行,启动并设置开机自启:
1
sudo systemctl enable ssh --now
4. 防火墙/安全组
Ubuntu:
1
2
sudo ufw status
sudo ufw allow 22/tcp
CentOS/RHEL:
1
sudo firewall-cmd --permanent --add-service=ssh && sudo firewall-cmd --reload
5. SSH 配置文件
检查:
1
sudo nano /etc/ssh/sshd_config
关键项:
1
2
3
Port 22
PermitRootLogin no
PasswordAuthentication yes
修改后重启:
1
sudo systemctl restart ssh
6. 日志排查
Ubuntu:
1
sudo tail -f /var/log/auth.log
CentOS:
1
sudo tail -f /var/log/secure
7. 权限检查
确保 .ssh 目录和文件权限正确:
1
2
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
通过本文的步骤,我们完成了以下几件事:
使用 SSH 登录懒猫算力仓;
配置 SSH 公钥,实现免密登录;
利用 ~/.ssh/config 简化连接命令;
配置免密 sudo,省去输入密码的麻烦;
提供了常见 SSH 故障的排查方法。
这样配置下来,你就能在懒猫算力仓上实现“一步登录 → 直接 sudo”的丝滑体验。无论是本地调试模型,还是在局域网中远程部署服务,你都能做到“一条命令直达 root 权限”。这让懒猫算力仓真正变成了一个可以随时掌控的 AI 算力伙伴
---
## 懒猫算力仓初探(五):有了图形界面,算力仓轻松设置静态 IP
- URL: https://blog.no-claw.com/posts/2f8bfee6/
- 日期: 2025-09-25
- 分类: 懒猫微服 / 算力仓
- 标签: 懒猫微服, NAS
懒猫 AI 算力仓是一台功能完整的边缘算力设备,基于 Ubuntu 22.04 桌面系统,既能运行 Docker、容器编排,又能承担 AI 推理任务。与懒猫微服不同的是,算力仓自带完整的 图形界面(GUI),这让我们可以更直观地配置网络,比如今天要讲的——静态 IP。
本篇文章将详细讲解如何在算力仓桌面环境中,通过图形化方式(GUI)配置静态 IP 地址,并对比命令行 nmtui 的方法,让你的算力仓网络配置更稳、更容易管理。
为什么要设置静态 IP?
算力仓不像懒猫微服那样自带域名访问功能。
如果我们希望通过 SSH、VSCode 或 API 服务长期远程访问它,就必须让它的 IP 地址保持不变。否则,一旦 DHCP 动态分配的地址发生变化(比如重启路由器后),所有外部访问都会中断。
因此,静态 IP 是算力仓稳定访问的关键配置之一。
认识算力仓的图形界面
在「关于本机」中可以看到,算力仓运行的是 Ubuntu 22.04 LTS 桌面版。这意味着我们无需编辑配置文件或敲命令,而是可以直接通过系统设置进行网络管理。
右上角的系统托盘里有一个网络图标(有线网络是双箭头,Wi-Fi 是扇形波纹),这就是进入网络设置的入口。
在 Ubuntu 桌面下,图形界面的背后其实就是 NetworkManager 服务,也就是我们在服务器端使用 nmtui 或 nmcli 命令操作的同一个核心组件。
通过图形界面配置静态 IP(推荐方法)
下面我们一步步完成整个配置流程,从打开设置到验证网络。
打开网络设置
点击右上角网络图标 → 选择「设置」或「Network」。
也可以在左下角应用菜单中搜索「Settings」,在左侧栏中选择「网络(Network)」。
进入后,你会看到两种网络接口:
Wired(有线网络)
Wi-Fi(无线网络)
如果算力仓通过网线连接局域网,就选择有线网络进行配置。
选择要修改的网络接口
在「有线网络」一栏中,找到当前正在使用的接口(如 _Wired connection 1_),点击右侧的齿轮 ⚙️ 图标。
这个配置界面对应的是 NetworkManager 的“连接配置文件”。
每一个物理接口都有自己的配置文件,保存 IP、DNS、网关等信息。
切换为“手动(Manual)”模式
切换到 IPv4 标签页,可以看到「方法(Method)」下拉菜单:
Automatic (DHCP)
Manual
Link-Local Only
默认是「自动(DHCP)」,我们需要改为「手动(Manual)」。
修改后,会出现以下字段:
字段
示例值
说明
地址(Address)
192.168.1.100
设备的固定 IP
子网掩码(Netmask)
255.255.255.0 或 /24
家用路由器一般为 /24
网关(Gateway)
192.168.1.1
通常是路由器的地址
DNS
8.8.8.8, 1.1.1.1
推荐填写 Google 与 Cloudflare 的 DNS
注意:
IP 地址必须是未被占用的。可以先用命令测试:
1
ping 192.168.1.100
若返回 “Destination Host Unreachable”,说明该 IP 可用。
保存并验证
填写完成后,点击右下角的「Apply」或「Save」。
接着关闭再开启一次该网络接口,让配置立即生效。
(很多人忘记这步,导致 IP 没有更新。)
打开终端(Ctrl + Alt + T),输入:
1
ifconfig
确认你的新 IP 已生效后,测试外网连通性:
1
ping 8.8.8.8
若返回类似:
1
64 bytes from 8.8.8.8: icmp_seq=1 ttl=117 time=30.5 ms
说明静态 IP 设置成功。
配合懒猫微服实现远程访问
静态 IP 设置完成后,你就可以在懒猫微服上用端口转发工具,将算力仓的 SSH 服务暴露到微服域名下。
例如,通过微服域名的某个端口就能直接 SSH 登录算力仓,实现两端联动:
与命令行方式的对比
之前我们在懒猫微服上使用过 nmtui 命令配置静态 IP。
其实两种方法底层一致,都是操作 NetworkManager。
对比项
图形界面设置
nmtui 命令行设置
适合用户
新手、喜欢可视化操作
熟悉终端的高级用户
可视化程度
界面直观
字符交互
误操作风险
低
较高
生效方式
即时生效
需手动保存
使用场景
桌面系统(算力仓)
无图形环境(微服)
一句话总结:
算力仓有桌面,用图形界面;
微服是服务器,用命令行。
两者都基于同一个 NetworkManager,稳定、可靠又高效。
总结
通过图形界面设置静态 IP 的优点:
操作直观,无需命令;
自动生成配置文件,减少出错;
保存后立即生效;
可随时切换回 DHCP,灵活方便。
推荐实践:
懒猫微服(无界面) → 用命令行 nmtui
懒猫算力仓(有界面) → 用图形界面「设置」
懒猫微服靠命令行,算力仓靠图形界面。
底层同源,体验不同,效果一样稳。
---
## 懒猫算力仓初探(四):如何高效的向算力仓传输文件?
- URL: https://blog.no-claw.com/posts/7419d0c1/
- 日期: 2025-09-25
- 分类: 懒猫微服 / 算力仓
- 标签: 懒猫微服, NAS
在日常使用 懒猫 AI 算力仓 的过程中,文件传输是一个绕不开的话题。和微服不一样的是,算力仓没有运行专门的网盘服务。在使用过程中,不管你是想把本地写好的程序同步到算力仓里运行,还是需要把推理结果、训练好的模型参数下载回客户端,都需要一个稳定、安全、方便的传输方案。
很多新手刚上手算力仓时,都会遇到类似问题:
“我怎么把代码传到算力仓里跑?”
“模型权重文件太大了,用 U 盘拷贝是不是更快?”
“我在笔记本写好脚本,能不能直接同步到算力仓?”
其实完全不用担心。虽然算力仓的核心是 NVIDIA Jetson AGX Orin 开发板,但它运行的系统本质上是一个 Linux 发行版(Ubuntu 22.04)。因此,大多数 Linux 服务器的文件传输方法,都可以无缝应用到懒猫 AI 算力仓上。
本文将从 命令行工具、图形化工具、高效同步、临时传输 和 云同步 五个方面,详细介绍客户端如何与懒猫 AI 算力仓进行文件传输,帮助你构建一个高效、稳定的工作流。
一、前置准备:确认算力仓的访问方式
在传文件之前,首先要确认两件事:算力仓的 IP 地址, ssh 服务运行状态。
1. 查看 IP 地址
路由器后台:如果算力仓接入了家里的路由器,可以在后台设备列表中直接找到分配给它的内网 IP 地址。
命令行:如果你能直接访问算力仓终端,执行以下命令:
1
ip addr show
找到类似 192.168.x.x 的地址即可。
建议:为避免每次重启导致 IP 变化,可以在路由器中绑定 固定 IP。
2. 开启 SSH 服务
懒猫 AI 算力仓默认预装了 OpenSSH 服务,因此只要知道用户名和密码,就能远程登录和传输文件。
默认用户名:nvidia
默认密码:nvidia
当然,更推荐配置 SSH Key 免密登录,更安全也更高效。具体配置可以参考这篇文章:算力仓 SSH 免密配置教程。
二、SCP:最经典的命令行传输方式
SCP(Secure Copy Protocol) 是 Linux/Unix 世界中最常见的文件传输方式,基于 SSH 加密,操作简单。
常见用法
上传文件到算力仓
1
scp file.txt nvidia@192.168.x.x:~
从算力仓下载文件到客户端
1
scp nvidia@192.168.x.x:~/1.txt .
上传整个目录
1
scp -r ./localdir nvidia@192.168.5.50:/home/nvidia/
使用体验
SCP 的优点是 原生支持、无需额外安装,适合偶尔传输单个文件或小目录;缺点是大文件速度一般,且不支持断点续传。
三、SFTP:更灵活的文件传输
SFTP(SSH File Transfer Protocol) 同样基于 SSH,但它支持交互式操作,功能更丰富。
命令行示例
1
2
3
4
5
6
7
8
sftp nvidia@192.168.5.50
sftp> lpwd # 查看本地目录
sftp> pwd # 查看远程目录
sftp> put data.csv # 上传文件
sftp> get result.log # 下载文件
sftp> put -r models/ backup/ # 上传目录
sftp> get -r logs/ ./ # 下载目录
图形化客户端
如果你不喜欢命令行,可以选择图形化工具:
WinSCP(Windows)—— 界面类似资源管理器,拖拽即可传输文件。
FileZilla(跨平台)—— 免费开源,支持 SFTP,操作直观。
参考文章:FileZilla 连接算力仓。
适用场景:SFTP 非常适合需要频繁上传/下载小文件的用户,尤其是习惯拖拽操作的开发者。
四、rsync:大文件与目录同步神器
如果你需要频繁同步数据集、模型权重或代码目录,rsync 是比 SCP 更高效的工具。
常见命令
上传目录
1
rsync -avz ./localdir/ nvidia@192.168.x.x:/home/nvidia/remote-dir
下载目录
1
rsync -avz nvidia@192.168.x.x:/home/nvidia/remote-dir ./localdir/
断点续传大文件
1
rsync -avz --partial --progress largefile.tar nvidia@192.168.x.x:/home/nvidia/
使用体验
速度快(仅传输差异部分)
支持断点续传
非常适合同步大文件和频繁更新的项目
例如,如果你在本地不断修改代码仓库,rsync 能快速同步更新的部分,而无需重新传输整个目录。
五、临时传输:快速“丢一个文件”
有时候你只是想传一个临时文件,不想折腾 SSH,这时可以用 HTTP 服务 或 curl。
Python 内置 HTTP 服务
1
2
cd ~/myfiles
python3 -m http.server 8000
在算力仓浏览器中访问:
1
http://192.168.x.x:8000/
在算力仓终端使用如下命令即可下载文件。
curl 下载
1
curl -O http://192.168.x.x:8000/target_file.txt
还可以显示进度条:
1
curl -# -O http://192.168.x.x:8000/large_file.iso
或指定保存路径:
1
curl -o /path/to/save/file.txt http://192.168.x.x:8000/file.txt
适用场景:临时传小文件时非常好用,几乎不需要额外配置。
六、云盘与同步工具:跨设备协作更高效
因为算力仓有 GUI 桌面,所以可以当作 PC 来用,如果你在笔记本和算力仓之间频繁切换,可以考虑 云同步工具。
Dropbox / Google Drive / OneDrive:跨平台支持,需要在算力仓上安装客户端。
**Syncthing**:去中心化文件同步工具,实时更新、速度快。
Git + GitHub/GitLab:适合代码文件的版本控制与协作开发。
使用经验:我个人推荐 Syncthing + Git 的组合。Syncthing 用来自动同步数据和配置文件,而 Git 用来管理代码版本。这样既能保证文件实时更新,又能避免版本混乱。
七、选择建议与总结
常见场景下的最佳选择:
偶尔传输小文件 → scp
图形化体验 → SFTP + WinSCP/FileZilla
大文件和频繁同步 → rsync
临时丢文件 → Python HTTP 服务
云盘与同步工具 → Syncthing + Git
对大多数懒猫 AI 算力仓用户来说,SCP + rsync 足以满足日常需求。如果你希望构建更稳定的工作流,可以结合 云盘/同步工具,让文件管理更加自动化。
八、结语
懒猫 AI 算力仓不仅仅是一台小型 AI 超算,它同时也是一个灵活的 Linux 工作站。掌握不同的文件传输方式,能让你在写代码、跑实验、部署服务时更加高效。
如果你只是偶尔传输文件,用 scp 就足够。
如果你是日常开发者,建议结合 SFTP + rsync。
如果你追求自动化与协作,Syncthing + Git 会让你事半功倍。
未来,当你完全把算力仓融入到工作流时,它就不仅是一个推理/训练机器,而是真正的 远程 AI 工作站。
掌握好文件传输的方法,你会发现:懒猫 AI 算力仓不仅提供算力,更能成为你日常开发中的最佳助手。
---
## 懒猫算力仓初探(二):懒猫微服如何绑定算力仓?
- URL: https://blog.no-claw.com/posts/cbd8657f/
- 日期: 2025-09-25
- 分类: 懒猫微服 / 算力仓
- 标签: 懒猫微服, NAS
在上一篇《懒猫算力仓初探(一):久违的开箱》中,我们已经认识了这台看似“小巧”,实则暗藏强劲算力的设备——懒猫 AI 算力仓。很多朋友拿到设备后,最关心的问题就是:如何让这台算力仓与懒猫微服生态结合,从而发挥它真正的威力?今天这一篇,就带大家详细走一遍绑定流程,并结合实际体验,聊聊我在操作过程中遇到的细节和思考。
为什么要绑定算力仓?
算力仓从硬件层面来说就是一台高性能的小型 AI 算力设备,但如果只是孤立地插上网线、运行系统,它就像一台“裸机”,并不能和我们常用的软件生态融合。懒猫微服作为一个容器化的应用生态,本身聚合了丰富的 AI 应用、插件和社区资源。把算力仓绑定到微服,就意味着你不仅能在局域网内访问算力,还能直接调用它的 API 来支撑各类应用。
比如:
在浏览器中实时调用算力仓进行网页总结。
在微服的应用市场中安装 AI 辅助工具,直接调用本地算力而不是远程云端。
在需要高隐私的场景下,本地算力仓可以避免敏感数据上传到外部云环境,数据留存在局域网。
一句话总结:绑定就是打通生态,让算力仓成为微服体系中的“外挂显卡”。没有绑定,它只是一个单机的开发板;绑定之后,它才真正融入到懒猫微服的生态网络里。
步骤一:准备 AI 浏览器
绑定流程的第一步是安装懒猫 AI 浏览器。这款浏览器是懒猫微服官方推出的专用入口,它在 Chromium 的基础上去除了 Google 的追踪代码,主打隐私和轻量。同时,它内置了算力仓插件,可以让浏览器与算力仓直接通信。
很多人可能会问:“为什么一定要用它?我能不能直接用 Chrome 或 Edge?”答案是理论上可以,但官方推荐 AI 浏览器,是因为它不仅省去了自己安装插件的步骤,还能确保和算力仓的兼容性。尤其是对小白用户而言,开箱即用的体验比手动安装插件要友好得多。
安装完成后,浏览器会自动跳转到 https://ai..heiyu.space/ 这个地址。这其实是一个专门的配置门户,我们后续的绑定操作就是在这里完成的。
步骤二:下载安装算力仓插件
如果你使用的不是 AI 浏览器,也没关系。官方同样提供了独立的插件下载入口:
1
https://extensions-ai..heiyu.space/
在这里下载安装后,效果和 AI 浏览器自带插件是完全一致的。区别只在于操作步骤多了一点点,但对于喜欢用主力浏览器的人来说,这个方式会更灵活。
插件的核心作用,就是在网页端注入算力仓的能力。比如,你在 Google 搜索页面时,可以直接点击总结按钮,让算力仓帮你对搜索结果做归纳。这一刻,你就能直观感受到“本地 AI 算力”与日常浏览体验结合的爽感。
步骤三:进入算力仓绑定界面
当浏览器启动并加载插件后,在右上角点击设置按钮,就能看到绑定算力仓的入口。界面非常简洁,主要就是一个“添加算力仓”的流程。
这里需要注意,绑定并不是直接输入算力仓的 IP,而是先让微服客户端与算力仓建立信任关系。换句话说,微服扮演的是“中介人”的角色,它负责把应用和算力仓之间的调用关系建立起来。这样设计的好处是:用户不需要关心底层 API 地址,只需要点几下就能完成对接。
步骤四:开启微服客户端
绑定算力仓之前,你需要先启动本地的懒猫微服客户端。在客户端里输入你的微服名称,然后等待连接成功。
这一环节是整个流程的关键:只有当微服客户端处于运行状态时,它才能帮你发现微服和算力仓。算力仓和微服的关系,就像 NAS 和群晖套件一样,必须要有一个“宿主环境”来承载它。
步骤五:添加算力仓
当微服和客户端连接成功后,你会在界面里看到“添加算力仓”的按钮。点击之后,微服就会开始在局域网内扫描设备。
这里有一个细节体验:只要算力仓接上网线,就能被自动发现。猜测它通过 mDNS/Bonjour 的方式在局域网广播,客户端自然就能识别出来。对比市面上一些设备需要手动输入 IP 或者搞复杂的端口映射,懒猫算力仓的体验确实要顺滑很多。
值得一提的是,算力仓的设计允许你绑定多台设备。如果你财力雄厚,可以一次性接入多个算力仓,微服会自动帮你做任务分配。这意味着什么?意味着你可以搭建一个“家庭 AI 超算集群”。对于 AI 极客来说,这是一个非常诱人的玩法。
步骤六:设备配对
扫描到算力仓之后,在设备的背面你会看到一个“M”按键。只要按下它,就能完成和微服的配对。
为什么需要这一步?因为这是设备级的安全认证。只有物理触发了 M 键,微服才会确认你是设备的真实拥有者,避免别人通过网络把你的算力仓“偷走”。
按下之后,你会在客户端看到“配对成功”的提示,这意味着你的算力仓正式加入到了微服生态。
步骤七:绑定完成,功能解锁
绑定完成后,你会在微服的设置页面里看到算力仓的选项。此时,你就可以选择不同的模型来运行对应功能。
比如:
在网页总结时选择轻量级的 7B 模型,保证速度快。
在本地知识库问答时,调用 13B 模型,兼顾准确性和上下文理解。
这一切,都是通过一个简洁的界面完成的,几乎不需要你去敲命令。
使用体验:Google 首页总结
绑定成功的第一个直观体验,就是在 Google 首页上使用总结功能。以前你可能需要安装第三方插件,或者把内容复制到 ChatGPT 界面里再问,现在直接在浏览器里点一下按钮,就能调用本地算力仓来生成总结。
更重要的是,这个总结是本地推理完成的,你的数据不会上传到外部云端。这对于关心隐私的人来说,是一个巨大的优势。
总结与思考
整个绑定流程下来,我最大的感受有三点:
上手难度低
无论是 AI 浏览器的内置插件,还是局域网自动发现的机制,都极大降低了新手的门槛。你不需要懂网络配置,也不需要手动输入复杂命令,基本“下一步、下一步”就能完成。
安全性设计合理
配对环节必须按下 M 键,这是一种物理确认机制,避免了远程劫持的风险。这点比纯软件的配对更让人安心。
扩展性强
从理论上讲,你可以无限接入算力仓,组建一个家庭算力集群。未来随着 AI 模型越来越大,这种本地集群的玩法可能会越来越受欢迎。
在我看来,懒猫算力仓和懒猫微服的结合,是一次“硬件 + 软件生态”的双重升级。前者提供了扎实的算力基础,后者则给了它用武之地。两者结合之后,才算真正完成了从设备到应用的闭环。
未来我会继续分享在算力仓上部署不同 AI 应用的体验,看看它在文本生成、图像处理、甚至视频推理等场景中的表现。敬请期待《懒猫算力仓初探(三):如何 SSH 登陆以及配置免密 sudo?》。
---
## Easysearch 可视化升级:无需额外部署 UI 软件
- URL: https://blog.no-claw.com/posts/a378e5b1/
- 日期: 2025-09-20
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
最近 Easysearch 上线了一个非常实用的新功能 —— 内置 UI 可视化工具。它可以随着集群一并部署,无需额外安装任何插件或第三方软件。相比之下,虽然 Console 已经比 Kibana 简化了很多,但这个内置 UI 在易用性和轻量化方面更进一步。只需访问 /_ui 路径,就能直接进入可视化页面。
集群监控开箱即用
登录后,首页就能看到集群的核心监控信息,包括节点数量、分片分布和索引情况。
默认面板已经内置了常用指标:节点、分片、索引级别的监控数据都能直接查看。
许多用户期盼已久的 字段存储图 也终于回归,让存储空间的使用情况一目了然。整体上,基本所需的监控指标都齐备了。
内置开发工具,所见即所得
其实我最喜欢的功能是这个开发工具,这样在部署集群之后就可以很容易的写 DSL 语句来执行来。不需要调用 REST API 或者暗转跟其他工具,Easysearch 部署好之后,所见即所得。
这意味着集群部署完成后,你就能立刻:
编写并执行查询语句
测试复杂的聚合和过滤
实时查看返回结果
这种“所见即所得”的体验,大大提升了调试和使用的效率。
别名与索引模板管理
除了监控和开发工具,界面中还集成了 别名管理 和 索引模板 功能。该有的功能差不多都有了。
这让日常配置和维护变得更加直观,无需频繁在命令行和 JSON 文件之间来回切换。
总结
这次更新,可以说是把一个“微缩版 Console”直接集成进了 Easysearch 内部:
开箱即用,无需额外部署 UI
监控指标完善,核心信息一目了然
集成开发工具,提升 DSL 使用体验
支持别名和索引模板,简化日常维护
对于开发者和运维人员来说,这无疑让 Easysearch 的使用更加高效、轻量化,真正实现了“安装即用”。
---
## 懒猫微服实战入门(二十六):Obsidian 用 GIT 同步总是冲突,果断存到懒猫微服了
- URL: https://blog.no-claw.com/posts/a1d9284e/
- 日期: 2025-09-19
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服
三年前,朋友推荐我使用 Obsidian 记笔记。除了炫酷的插件功能之外,最吸引人的就是云同步。虽然 Obsidian 提供了官方同步功能,但价格不菲,所以我们一直在寻找平替方案。朋友选择了官方同步,而我当时用了 Git 来同步。
看似解决了问题,但 Git 同步的体验并不好:多端同步时经常遇到冲突,每次写完笔记都要处理一堆合并,既麻烦又影响心情。这件事困扰了我很久。
直到后来入手了 懒猫微服,问题才彻底缓解,所以决定把这段折腾经历记录下来。
用懒猫微服替代 Git,同步更顺畅
懒猫提供了基于 WebDAV 协议的网络存储,天然支持 Obsidian 的同步需求。它不像 Git 那么复杂,不需要 commit/push,也不会频繁出现冲突。只要配置一次,就能稳定、流畅地跑起来。
所以我把 Obsidian 笔记直接备份到懒猫微服。在网盘中可以看到 WebDAV 的域名、用户名和密码。懒猫微服的转发没有任何限制,443 端口也能直接使用。这样即使在外面,也能随时访问。
插件安装与配置
首先在 Obsidian 插件市场搜索并安装第三方插件 【Remotely Save】。
在【Remotely Save】中,选择 WebDAV 模式,然后输入服务器地址:【地址+端口+目录路径】。
我的地址是:
1
https://file.name.heiyu.space/dav/ob_notes
由于 /dav 是网盘的根目录,我新建了一个文件夹 ob_notes 来存放笔记,方便结构化管理。
输入懒猫微服提示的用户名和密码,鉴权方式保持 basic,然后检查服务器连接。如果目录不存在,就会在检查时报错。
开启自动同步
我设置了 自动运行,并且每 10 分钟同步一次。毕竟是自己的微服,不存在 API 限制,也不用担心额外费用。更重要的是,我开启了 双向同步:
本地改动会同步到微服;
微服上的改动也会同步回本地。
再也不用担心 Git 冲突,写完笔记就能安心同步。
这是我在网盘上的结果,保存的瞬间,笔记就自动完成同步:
保存会自动更新到服务端
本地新建或删除文件,也会实时更新到微服
微服新建或删除文件,本地也会自动保持一致
总结
写到这里,其实结论很简单:懒猫微服彻底解决了我在 Obsidian 上被 Git 同步折磨的痛点。相比频繁冲突的 Git,懒猫微服的 WebDAV 同步方式更简单、更稳定,还能随时随地使用。
如果你也在用 Obsidian,又还在为同步问题发愁,不妨果断试试懒猫微服——真正做到写完即同步,省心又安心。
一句话:Obsidian 最佳拍档,果断选懒猫微服。
---
## 懒猫微服实战入门(二十七):KSpeeder 加速,让 Docker 镜像不再超时
- URL: https://blog.no-claw.com/posts/6300bfd5/
- 日期: 2025-09-19
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服
在国内使用 Docker 时,镜像拉取缓慢、超时以及镜像源不稳定几乎是开发者的日常困扰。虽然市面上存在一些第三方加速镜像,但它们往往会随着时间逐渐失效,或者更新不够及时。
如今,我们可以在 懒猫微服 的应用商店中一键部署 KSpeeder,并结合懒猫自带的 端口转发、内网穿透、可视化管理 等功能,使 Docker 镜像加速更加高效、易用和可维护。
为什么选择懒猫微服
在传统环境下,部署像 KSpeeder 这样的工具通常需要:
手动构建镜像和容器;
自行配置网络和转发规则;
解决外部访问和权限问题。
而在懒猫微服上,这些复杂步骤被大幅简化:
应用商店一键安装:直接获取 KSpeeder,无需自行构建镜像或处理依赖问题;
内网穿透:无需复杂配置,即可在异地访问加速服务;
这使得 KSpeeder 不再是一个需要额外维护的独立工具,而是融入懒猫微服生态的“开箱即用”解决方案。
KSpeeder 的功能特性
在懒猫微服的支持下,KSpeeder 能够提供以下关键能力:
多镜像并发下载
支持同时从多个镜像源并行下载,就像开启多条高速通道,一次性快速拉取多个镜像,节省大量等待时间。
动态负载均衡
智能选择最优镜像源,并根据实时情况分配下载任务,充分利用带宽和资源,始终保持较高下载速度。
断点续传
下载中断后可从中断位置继续,无需重新开始,尤其在拉取大镜像时能有效节省时间和流量。
实时监控
提供可视化界面,直观展示下载进度、镜像源利用率等信息,方便用户动态调整策略,确保任务顺利完成。
特别感谢社区群友 Peterpig 的帮助,由于 KSpeeder 的限制,镜像源无法直接通过 ingress 暴露端口,因此这里使用了懒猫微服的 端口转发功能,保证服务可用。
配置与使用
1. 修改 hosts 文件
在本地客户端上,将 registry.linkease.net 指向懒猫微服所在主机的 IP 地址:
1
sudo vim /etc/hosts
在文件最后一行添加:
1
192.168.x.x registry.linkease.net
(例如:192.168.5.128 registry.linkease.net)
完成后刷新 DNS 缓存:
1
2
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder
通过 ping 验证是否生效:
1
ping registry.linkease.net
2. 拉取镜像
完成配置后,就可以直接通过 KSpeeder 加速源拉取镜像:
1
docker pull registry.linkease.net:5443/image:tag
3. Docker 引擎配置
我使用的是 Orbstack,可以在其配置文件中加入镜像加速源,让 Docker 默认通过懒猫微服上的 KSpeeder 服务:
1
"registry-mirrors": ["https://registry.linkease.net:5443"]
这样每次执行 docker pull 时,就无需手动添加前缀:
1
docker pull nginx
可以通过查看配置文件确认:
1
2
3
4
5
6
cat ~/.orbstack/config/docker.json
{
"registry-mirrors" : [
"https:\/\/registry.linkease.net:5443"
]
}
效果验证
配置完成后,可以在懒猫微服的 Web 界面直观地查看镜像下载状态与速度。
实际测试表明,借助懒猫微服的 端口转发和可视化管理能力,KSpeeder 能够稳定高效地运行,显著缓解 Docker 镜像下载慢和中断的问题。
总结
KSpeeder 提供了并发下载、负载均衡、断点续传和实时监控等功能,有效提升 Docker 镜像拉取效率。
懒猫微服 则通过应用商店、端口转发、内网穿透和可视化管理,最大化发挥了 KSpeeder 的价值,让开发者能够快速部署、低成本维护。
在频繁依赖 Docker 镜像的开发环境中,“KSpeeder + 懒猫微服” 是一个高效、稳定、可扩展的组合方案,值得推荐。
---
## 懒猫微服实战入门(二十九):域名解析与 2.5G 测速体验
- URL: https://blog.no-claw.com/posts/63e9bc75/
- 日期: 2025-09-19
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服
很多朋友在用 NAS 或者家用服务器的时候,都会习惯直接用 IP 地址来访问设备。但当我开始深入折腾 懒猫微服时,发现它的逻辑和传统 NAS 有点不一样:懒猫不是给你一个裸 IP,而是直接分配一个 **域名 <机器名>.heiyu.space**。
这就意味着,不管你是在内网还是外网,访问方式都是一致的——直接用域名即可。对习惯了公网/私网切换的我来说,这个设计一开始挺新奇,也忍不住想探索背后的机制和性能表现。于是就有了这篇文章:一次围绕 懒猫微服网络访问和测速表现的实战记录。
你可以使用 dig AAAA <机器名>.heiyu.space 来解析这个域名,背后是一个 IPV6 的地址。尝试破解了很久也没弄懂这个这个具体的转发机制,不过结论就是,不管你在内网还是互联网上访问这个域名都没问题。当然如果你内网有代理的话,流量很可能通过代理从外边转一圈再回来,这个需要手动改下规则。
设备环境
Macbook Pro M2 Pro,网卡支持 WiFi 6E
懒猫微服,内置 AX210 无线网卡
中兴路由器 1G,小米路由器 2.5G
先说结论,懒猫微服的无线网卡还是很好的。不过也遇到了在 UPS 附近出现严重干扰的情况。
访问懒猫微服的方式
域名访问:<设备名>.heiyu.space
有线 IP 地址
无线 IP 地址
为什么要把有线和无线 IP 分开说?因为逻辑上它和云服务器一样:用公网 IP 就是走公网,用私有 IP 就是走内网。
查看协商速率
在 Linux 环境下,可以使用 iw wlp4s0 link 查看 WiFi 的连接情况:
1
watch -n 1 iw wlp4s0 link
如果提示 Not connected.,说明当前 WiFi 没有连接。
测速工具与对比
为了保证测试结果客观,我前后测试了很久,还顺带拿黑群晖做了对比。测试环境涵盖了 1G 和 2.5G 的场景,工具主要用了 iperf3 和 LibreSpeed。
不过在 2.5G 的小米路由器环境下,经常遇到抽风的情况,这点要单独吐槽一下。
网络测速
有限可以跑满 1G,WIFI6 协商速率可以跑满 2401Mbps。
基本也是 WIFI6 协商满速 2401Mbps
纯有线测速
使用 IP 测速度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[ ID] Interval Transfer Bitrate
[ 5] 0.00-1.00 sec 113 MBytes 948 Mbits/sec
[ 5] 1.00-2.00 sec 113 MBytes 946 Mbits/sec
[ 5] 2.00-3.00 sec 113 MBytes 944 Mbits/sec
[ 5] 3.00-4.00 sec 113 MBytes 944 Mbits/sec
[ 5] 4.00-5.00 sec 113 MBytes 945 Mbits/sec
[ 5] 5.00-6.00 sec 112 MBytes 943 Mbits/sec
[ 5] 6.00-7.00 sec 112 MBytes 936 Mbits/sec
[ 5] 7.00-8.00 sec 112 MBytes 943 Mbits/sec
[ 5] 8.00-9.00 sec 112 MBytes 943 Mbits/sec
[ 5] 9.00-10.00 sec 112 MBytes 942 Mbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate
[ 5] 0.00-10.00 sec 1.10 GBytes 943 Mbits/sec sender
[ 5] 0.00-10.00 sec 1.10 GBytes 941 Mbits/sec receiver
基本稳定在 940Mbps 左右。
纯无线测速
Macbook Pro 无线 对 懒猫有线
Macbook Pro 有线对懒猫无线
2.5G 测速
可以看到这里已经是协商到了 2500Mbps。
即便是 WiFi7 路由器,由于终端限制,TX 协商依旧只有 2401Mbps。
Macbook Pro 和懒猫微服有线对打(2.5G)
在商店里的 Libre Speed 中测速如下:(APP 内部测速:)
浏览器测速(似乎是有些损失):
不清楚商店是不是有什么限制,但是如果自己用 playground dokcer 部署就没啥问题:
1
pg-docker run -p 2333:80 registry.lazycat.cloud/librespeed:23.5.12
而在 openspeetest 似乎是有 bug:
使用域名(全有线)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
iperf3 -c micro.heiyu.space (base) 12:15:39
Connecting to host micro.heiyu.space, port 5201
[ 6] local fc03:1136:3803:e43f:1f7e:9149:3f1f:0 port 59188 connected to fc03:1136:384f:313:a637:437:d22b:0 port 5201
[ ID] Interval Transfer Bitrate
[ 6] 0.00-1.00 sec 87.2 MBytes 732 Mbits/sec
[ 6] 1.00-2.00 sec 91.2 MBytes 765 Mbits/sec
[ 6] 2.00-3.00 sec 90.6 MBytes 760 Mbits/sec
[ 6] 3.00-4.00 sec 91.2 MBytes 765 Mbits/sec
[ 6] 4.00-5.00 sec 92.0 MBytes 772 Mbits/sec
[ 6] 5.00-6.00 sec 92.6 MBytes 777 Mbits/sec
[ 6] 6.00-7.00 sec 90.9 MBytes 763 Mbits/sec
[ 6] 7.00-8.00 sec 90.8 MBytes 761 Mbits/sec
[ 6] 8.00-9.00 sec 91.4 MBytes 767 Mbits/sec
[ 6] 9.00-10.00 sec 89.5 MBytes 751 Mbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate
[ 6] 0.00-10.00 sec 907 MBytes 761 Mbits/sec sender
[ 6] 0.00-10.00 sec 906 MBytes 760 Mbits/sec receiver
iperf Done.
❰xu❙~❱✔≻ iperf3 -c micro.heiyu.space -R (base) 12:16:12
Connecting to host micro.heiyu.space, port 5201
Reverse mode, remote host micro.heiyu.space is sending
[ 6] local fc03:1136:3803:e43f:1f7e:9149:3f1f:0 port 59310 connected to fc03:1136:384f:313:a637:437:d22b:0 port 5201
[ ID] Interval Transfer Bitrate
[ 6] 0.00-1.00 sec 168 MBytes 1.41 Gbits/sec
[ 6] 1.00-2.00 sec 178 MBytes 1.49 Gbits/sec
[ 6] 2.00-3.00 sec 184 MBytes 1.55 Gbits/sec
[ 6] 3.00-4.00 sec 182 MBytes 1.52 Gbits/sec
[ 6] 4.00-5.00 sec 182 MBytes 1.52 Gbits/sec
[ 6] 5.00-6.00 sec 181 MBytes 1.52 Gbits/sec
[ 6] 6.00-7.00 sec 168 MBytes 1.41 Gbits/sec
[ 6] 7.00-8.00 sec 176 MBytes 1.47 Gbits/sec
[ 6] 8.00-9.00 sec 185 MBytes 1.55 Gbits/sec
[ 6] 9.00-10.00 sec 177 MBytes 1.49 Gbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate Retr
[ 6] 0.00-10.00 sec 1.74 GBytes 1.49 Gbits/sec 122 sender
[ 6] 0.00-10.00 sec 1.74 GBytes 1.49 Gbits/sec receiver
iperf Done.
如果换 IP 可以跑满
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
❰xu❙~❱✔≻ iperf3 -c 192.168.31.9 -R (base) 12:15:02
Connecting to host 192.168.31.9, port 5201
Reverse mode, remote host 192.168.31.9 is sending
[ 5] local 192.168.31.27 port 58702 connected to 192.168.31.9 port 5201
[ ID] Interval Transfer Bitrate
[ 5] 0.00-1.00 sec 278 MBytes 2.33 Gbits/sec
[ 5] 1.00-2.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 2.00-3.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 3.00-4.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 4.00-5.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 5.00-6.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 6.00-7.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 7.00-8.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 8.00-9.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 9.00-10.00 sec 280 MBytes 2.35 Gbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate Retr
[ 5] 0.00-10.00 sec 2.73 GBytes 2.35 Gbits/sec 0 sender
[ 5] 0.00-10.00 sec 2.73 GBytes 2.35 Gbits/sec receiver
iperf Done.
❰xu❙~❱✔≻ iperf3 -c 192.168.31.9 (base) 12:15:20
Connecting to host 192.168.31.9, port 5201
[ 5] local 192.168.31.27 port 58824 connected to 192.168.31.9 port 5201
[ ID] Interval Transfer Bitrate
[ 5] 0.00-1.00 sec 283 MBytes 2.37 Gbits/sec
[ 5] 1.00-2.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 2.00-3.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 3.00-4.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 4.00-5.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 5.00-6.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 6.00-7.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 7.00-8.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 8.00-9.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 9.00-10.00 sec 279 MBytes 2.34 Gbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate
[ 5] 0.00-10.00 sec 2.73 GBytes 2.35 Gbits/sec sender
[ 5] 0.00-10.00 sec 2.73 GBytes 2.35 Gbits/sec receiver
iperf Done.
Macbook Pro 无线对 懒猫 2.5G 有线
懒猫无线对 Macbook Pro 2.5G 有线
懒猫的无线网卡是 AX210,这么看这个吞吐量还是不错的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
iperf3 -c <懒猫无线地址>
[ ID] Interval Transfer Bitrate
[ 5] 0.00-1.00 sec 135 MBytes 1.13 Gbits/sec
[ 5] 1.00-2.00 sec 181 MBytes 1.52 Gbits/sec
[ 5] 2.00-3.00 sec 190 MBytes 1.59 Gbits/sec
[ 5] 3.00-4.00 sec 202 MBytes 1.69 Gbits/sec
[ 5] 4.00-5.00 sec 202 MBytes 1.70 Gbits/sec
[ 5] 5.00-6.00 sec 206 MBytes 1.72 Gbits/sec
[ 5] 6.00-7.00 sec 202 MBytes 1.69 Gbits/sec
[ 5] 7.00-8.00 sec 208 MBytes 1.75 Gbits/sec
[ 5] 8.00-9.00 sec 205 MBytes 1.72 Gbits/sec
[ 5] 9.00-10.00 sec 204 MBytes 1.71 Gbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate
[ 5] 0.00-10.00 sec 1.89 GBytes 1.62 Gbits/sec sender
[ 5] 0.00-10.01 sec 1.89 GBytes 1.62 Gbits/sec receiver
iperf Done.
iperf3 -c <懒猫无线地址> -R
[ ID] Interval Transfer Bitrate
[ 5] 0.00-1.00 sec 271 MBytes 2.27 Gbits/sec
[ 5] 1.00-2.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 2.00-3.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 3.00-4.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 4.00-5.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 5.00-6.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 6.00-7.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 7.00-8.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 8.00-9.00 sec 280 MBytes 2.35 Gbits/sec
[ 5] 9.00-10.00 sec 280 MBytes 2.35 Gbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate Retr
[ 5] 0.00-10.00 sec 2.73 GBytes 2.34 Gbits/sec 0 sender
[ 5] 0.00-10.00 sec 2.72 GBytes 2.34 Gbits/sec receiver
iperf Done.
❰xu❙~❱✔≻
无线对打
Macbook Pro 和懒猫微服无线对打(WIFI7),这个数字显然没有那么好看。咨询了路由器的售后,得到了这几个结论:
民用的 WIFI 都是半双工,所以一个路由器下两个设备对打速度减半
民用的 WIFI 网卡上传可能有阉割
即使用 4 收 4 发的路由器无法避免这个问题,因为通道不是独立的
RX 协商速率偶尔降到 6,可能与 beacon 帧有关(通信的东西不懂,先记下来)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
❰xu❙~❱✘≻ iperf3 -c <无线私有 IP> (base) 12:45:16
[ ID] Interval Transfer Bitrate
[ 5] 0.00-1.00 sec 74.0 MBytes 619 Mbits/sec
[ 5] 1.00-2.00 sec 78.4 MBytes 659 Mbits/sec
[ 5] 2.00-3.00 sec 73.8 MBytes 619 Mbits/sec
[ 5] 3.00-4.00 sec 69.3 MBytes 582 Mbits/sec
[ 5] 4.00-5.00 sec 64.4 MBytes 540 Mbits/sec
[ 5] 5.00-6.00 sec 63.2 MBytes 530 Mbits/sec
[ 5] 6.00-7.00 sec 68.9 MBytes 578 Mbits/sec
[ 5] 7.00-8.00 sec 65.6 MBytes 551 Mbits/sec
[ 5] 8.00-9.00 sec 68.8 MBytes 577 Mbits/sec
[ 5] 9.00-10.00 sec 63.2 MBytes 530 Mbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate
[ 5] 0.00-10.00 sec 690 MBytes 578 Mbits/sec sender
[ 5] 0.00-10.01 sec 689 MBytes 578 Mbits/sec receiver
iperf Done.
iperf3 -c <无线私有 IP> -R
[ ID] Interval Transfer Bitrate
[ 5] 0.00-1.00 sec 45.2 MBytes 379 Mbits/sec
[ 5] 1.00-2.00 sec 46.1 MBytes 387 Mbits/sec
[ 5] 2.00-3.00 sec 46.1 MBytes 387 Mbits/sec
[ 5] 3.00-4.00 sec 46.0 MBytes 386 Mbits/sec
[ 5] 4.00-5.00 sec 45.8 MBytes 384 Mbits/sec
[ 5] 5.00-6.00 sec 46.2 MBytes 388 Mbits/sec
[ 5] 6.00-7.00 sec 45.8 MBytes 385 Mbits/sec
[ 5] 7.00-8.00 sec 45.3 MBytes 380 Mbits/sec
[ 5] 8.00-9.00 sec 45.8 MBytes 383 Mbits/sec
[ 5] 9.00-10.00 sec 46.0 MBytes 387 Mbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate Retr
[ 5] 0.00-10.01 sec 462 MBytes 387 Mbits/sec 0 sender
[ 5] 0.00-10.00 sec 458 MBytes 384 Mbits/sec receiver
iperf Done.
结尾(总结 + 推荐)
整体折腾下来,我的结论是:
懒猫微服在域名接入上做了很贴心的优化,省去了公网/内网地址切换的麻烦;
不论是 1G 还是 2.5G 的场景,有线和无线的表现都相当稳定,无线网卡(AX210)在合适环境下也能发挥出很不错的吞吐;
测速中遇到的瓶颈,多数是路由器或测试环境的限制,而不是懒猫本身的问题。
如果你之前习惯用 NAS 或开发机折腾网络配置,那么上手懒猫微服一定会觉得“省心很多”。它不仅能满足日常访问需求,在高带宽场景下也能撑得住。
一句话总结:懒猫微服不仅仅是“内网穿透神器”,更是一台性能靠谱的迷你服务器。
---
## 懒猫微服实战入门(二十八):坏掉的 Windows 不要扔,硬盘插在懒猫上还能用
- URL: https://blog.no-claw.com/posts/768b4867/
- 日期: 2025-09-19
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服
最近整理旧电脑,发现有台 Windows 已经无法正常开机了。但是里面可能还存着一些以前的资
料和照片,还是想先看看有没有重要数据能救回来。这种情况电脑已经无法开机了,用 Windows PE 去修复启动基本没戏,于是我换了个思路:干脆把硬盘直接拆出来,接到懒猫微服上去。
说干就干,从箱子里翻出一个硬盘盒,把拆下来的硬盘装进去,再用 Type-C 数据线插到懒猫微服后面的接口,立刻就能当作外接硬盘使用。
df -TH 查看挂载情况
为了确认是否挂载成功,我在懒猫的终端输入了:
1
df -TH
输出结果如下:
1
2
Filesystem Type Size Used Avail Use% Mounted on
/dev/sdb3 fuseblk 2.1T 48G 2.0T 3% /lzcsys/run/media/系统
逐行解释一下:
设备:/dev/sdb3 → 说明识别到的硬盘分区是 sdb3;
文件系统类型:fuseblk → 代表这是通过 FUSE 驱动挂载的块设备。大多数情况下,这就是 NTFS 文件系统,因为 Linux 自带的 NTFS 支持往往依赖 ntfs-3g 这样的 FUSE 驱动;
容量情况:硬盘总大小 2.1 TB,目前只用了 48 GB,剩余 2.0 TB;
挂载点:/lzcsys/run/media/系统,懒猫系统会自动在 /media 下生成一个中文目录,名字就是 Windows 盘符的中文别名(比如“系统”)。
也就是说,这块 Windows 硬盘已经被完整挂载,可以直接访问。
lsblk 查看分区结构
为了更清楚地看到硬盘的分区情况,我又执行了 lsblk:
1
2
3
4
5
6
(base) lzcbox-029c588e ~ # lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sdb 8:16 0 1.8T 0 disk
├─sdb1 8:17 0 300M 0 part
├─sdb2 8:18 0 16M 0 part
└─sdb3 8:19 0 1.8T 0 part /lzcsys/run/media/系统
从结果可以看出:
sdb1 → 300 MB,这个就是 EFI 启动分区。在 Windows 系统里,这个分区通常用来存放启动管理器、Boot Loader 等信息;
sdb2 → 16 MB,这是 MSR(Microsoft Reserved)分区,Windows 在初始化 GPT 磁盘时会自动保留,用来为将来的系统更新留空间;
sdb3 → 1.8 TB 主分区,真正存放用户数据的地方,现在已经被挂载到了 /lzcsys/run/media/系统。
这套分区方案是 Windows 的标准 GPT 分区表结构。如果是 Linux,通常会看到 /boot、swap、/home 这样的挂载点。而 Windows 则用 EFI + MSR + 主分区的方式来管理。
进入数据目录
知道分区后,下一步就是进入挂载目录 /lzcsys/run/media/系统。
里面的目录结构几乎和 Windows 下看到的一样,核心的就是 Users 文件夹。
例如:
1
2
(base) lzcbox-029c588e ~ # ls /lzcsys/run/media/系统/Users
Administrator Default MyUser Public
在 MyUser 目录下,能找到 Desktop、Documents、Downloads、Pictures 等熟悉的文件夹。
这就是 Windows 用户的个人数据目录。只要进入对应的用户文件夹,就能直接把桌面文件、文档、照片、下载内容拷出来。
这里要注意:
有些系统文件夹(比如 Program Files)没必要去动,大部分个人数据都在 Users 目录下;
如果系统里开了 BitLocker 磁盘加密,那么在 Linux 下可能无法直接读,需要先解锁。
懒猫网盘直连
更方便的一点是:懒猫微服不仅仅能在终端挂载,还能在自带的 网盘界面里直接显示这个外接硬盘。
我打开懒猫的 Web 网盘页面,果然多出了一个名为“系统”的磁盘目录。
点进去以后,就能直接看到原来 Windows 里的 Users 文件夹。
这意味着:
不用进终端命令行;
直接用浏览器就能访问、下载文件;
大文件也能在局域网里高速传输。
对于普通用户来说,这个功能基本上等于“即插即用”。坏掉的 Windows 硬盘,插上懒猫就能当网盘用。
总结
整个过程体验下来,其实有点像“数据救援”。
一台电脑坏了,换做以前,大家可能要找专业修理店,或者买 制作 Windows PE 启动盘去折腾。
但有了懒猫微服,这件事变得非常简单:
硬盘拆下来,接上懒猫;
系统自动挂载,网盘直接显示;
打开 Users 文件夹,把需要的数据拷出来。
从头到尾几乎不需要额外的软件,Linux 自带的 FUSE 驱动就能把 NTFS 分区挂上去。
即使完全不会 Linux 命令行,也能通过懒猫的网盘访问到文件。
“坏掉的 Windows 不要扔,插在懒猫上还能用。”
---
## 懒猫微服实战入门(二十八):如何获取懒猫微服的私有地址
- URL: https://blog.no-claw.com/posts/4c7e3909/
- 日期: 2025-09-19
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服
在日常使用 懒猫微服(LazyCat Micro-Service) 的过程中,我们习惯于使用域名访问。如果你经常使用其他 NAS,就会问这个问题:如何知道一台机器在局域网里的私有地址?
很多朋友刚接触 NAS、Docker 或容器化环境时,都会觉得 IP 地址这种东西似乎理所当然。但等你真的要远程访问、配置代理或者排查网络时,就会发现,知道机器的私有地址是一件非常关键的事。
举几个典型的场景:
想通过 SSH、HTTP 等方式访问运行在懒猫微服上的服务;
需要进行 端口转发 或者把服务 内网穿透到公网;
在 Dockge 里启动容器后,访问的时候必须指定 IP 地址,否则反向代理会失败;
排查网络时,需要确认到底是哪一个 IP 地址在被使用。
换句话说,掌握如何获取私有地址,可以来排除一些域名上转发损失的问题。
虽然在客户端中可以看到网络地址,但是有阵子我修改 br-lan 网桥之后这里就不再显示了。所以才想了个办法来记录网络地址。下面将结合 Python 代码,介绍几种常见的获取私有地址的方法。
下面我会结合 Python 代码和一些常见的工具,介绍几种获取私有地址的常见做法,并分析它们的优缺点。
1. 为什么是“私有地址”
首先我们要搞清楚:什么是 私有地址?
私有地址是 RFC1918 标准定义的局域网网段,常见的有:
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16
这些 IP 地址不会直接出现在公网,而是专门留给局域网使用。比如:
你家路由器分配给手机的 192.168.31.2;
NAS 设备的 192.168.1.103;
Docker 创建虚拟网络时分配的 172.18.0.2。
所以,获取懒猫微服的“私有地址”,本质上就是找到它在局域网环境下的真实 IP。只有掌握了这个 IP,才能在排查问题的时候得心应手。
2. 最常用的快速方法:socket 连接法
在 Python 里,用 socket 模块就能很快拿到主要的私有 IP 地址。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import socket
def get_primary_ip():
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
# 不会真的发请求,只是借助路由表选择出网口
s.connect(('8.8.8.8', 80))
ip = s.getsockname()[0]
except Exception:
ip = '127.0.0.1'
finally:
s.close()
return ip
print(get_primary_ip())
运行后,输出大概是:
1
192.168.1.2
这就是懒猫微服当前在局域网里的主要出网地址。
原理说明
代码里 s.connect(('8.8.8.8', 80)) 看似是去连接 Google 的 DNS,其实并不会真正发起网络请求;
系统只是会查一遍路由表,决定要走哪个网卡出去;
然后 getsockname() 返回的就是这个网卡对应的本地 IP。
优缺点
优点:跨平台通用,代码简洁,结果非常准确。
缺点:依赖于有路由表,如果设备完全没有网络,可能就会返回 127.0.0.1。
这种方法非常适合:家用网络 / 有默认出网口 / 多网卡机器。
3. 另一种简单写法(但容易踩坑)
很多人还会写成:
1
2
import socket
print(socket.gethostbyname(socket.gethostname()))
这段代码通过 主机名解析 来获取 IP 地址,看似简单,但问题不少:
在很多系统里,它直接返回 127.0.0.1;
依赖 /etc/hosts 或 DNS 配置,如果没配好,就会失效;
在复杂网络环境下,很可能并不是你真正想要的地址。
所以,这种方法虽然一行就能搞定,但更适合在 主机名规范配置过的服务器,不太推荐在 NAS、容器、虚拟机这些环境里依赖。
4. 列出所有网卡的地址
如果环境比较复杂,比如同时有 Wi-Fi、以太网、虚拟网卡、容器网桥,只拿一个主要地址就不够了。这时候就需要直接列出所有接口的 IP,再从中筛选。
使用 ifaddr
安装:
1
pip install ifaddr --root-user-action=ignore
代码:
1
2
3
4
5
6
import ifaddr
adapters = ifaddr.get_adapters()
for adapter in adapters:
for ip in adapter.ips:
print(f"{adapter.nice_name}: {ip.ip}")
输出是:
1
2
3
eth0: 192.168.5.203
docker0: 172.17.0.1
lo: 127.0.0.1
使用 psutil
psutil 是更常见的运维库,信息更全面:
1
pip install psutil --root-user-action=ignore
代码:
1
2
3
4
5
6
7
import psutil, socket
addrs = psutil.net_if_addrs()
for iface, addr_list in addrs.items():
for addr in addr_list:
if addr.family == socket.AF_INET:
print(f"{iface}: {addr.address}")
输出会展示每一个网卡的 IPv4 地址:
适合在 虚拟化/容器/复杂网络环境 中使用。
5. 如何区分私有地址与公网地址
如果列出来的地址很多,还混杂了公网 IP,怎么办?可以用 ipaddress 模块过滤:
1
2
3
4
5
6
7
import ipaddress
def is_private(ip):
return ipaddress.ip_address(ip).is_private
print(is_private("192.168.5.203")) # True
print(is_private("8.8.8.8")) # False
这样就能快速区分哪些是真正的私有地址,避免混淆。
6. 实战场景举例
说了这么多,回到实际应用。获取私有地址到底能帮我们解决什么?
为容器配置反向代理
在 Caddyfile 中写:
1
reverse_proxy http://192.168.1.2:11434
就必须确认 192.168.5.203 真的是懒猫微服的地址。
跨设备访问服务
想用手机访问 NAS 提供的 Web 服务 http://192.168.1.2:8080,也必须提前知道 IP。
排查网络问题
当遇到容器内服务报错(403、502)时,确认是不是反向代理到了错误的 IP,就靠这一步。
7. 方法对比表
方法
示例代码
优点
缺点
适用场景
UDP 连接法
s.connect(('8.8.8.8', 80))
跨平台、常用、结果准确
需有路由/网络
获取主要私有地址
主机名解析
gethostbyname(gethostname())
简单,一行搞定
常返回 127.0.0.1,依赖配置
主机名已配置正确
ifaddr
ifaddr.get_adapters()
列出所有接口,跨平台
需安装库
多网卡、容器环境
psutil
psutil.net_if_addrs()
常见运维库,信息全面
需安装库
系统监控、诊断
ipaddress
ip_address(ip).is_private
能区分公网/私网
需结合其他方法
过滤私有地址
总结
获取懒猫微服的私有地址,其实就是获取机器在局域网里的 IP。
快速场景:用 socket + 8.8.8.8 方法,几乎百分百靠谱;
一行写法:gethostbyname(gethostname()),但要注意经常只会返回 127.0.0.1;
复杂环境:用 ifaddr 或 psutil 列出所有接口,再结合 ipaddress 做筛选。
掌握这些方法,你就能在 部署服务、调试容器、配置代理 时快速定位到懒猫微服的真实 IP,避免因为地址不明导致的各种问题。
以后无论是写 Caddyfile 配置、手机访问 NAS 服务,还是排查 403/502 错误,你都能从容地第一时间确认:**我的懒猫微服,到底在哪个 IP 上运行。
---
## 懒猫微服实战入门(三十):懒猫微服改装闺蜜机,一台设备的多重人生
- URL: https://blog.no-claw.com/posts/2c05ec4f/
- 日期: 2025-09-19
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服
在之前那篇文章《懒猫微服实战入门(三):懒猫智慧屏,我以为是地表最强电视盒子,结果竟然可以改装成闺蜜机?》里,我曾经抛出一个看似“不务正业”的想法——把懒猫微服改造成“闺蜜机”。
很多人看到这句话的第一反应是:“这玩意儿不是一个微服务器吗?还能当闺蜜机?”
没错,这正是这次改装的灵魂所在。我们要做的,不是再造一台设备,而是赋予它一个新的身份——一台有颜值、有功能、有灵魂的桌面伴侣。
从微服务器到桌面灵魂伴侣
懒猫微服(LazyCat MicroServer),是一台小巧、安静、功耗低却功能极强的家庭服务器。它能跑 Docker、能跑容器、能跑 AI 模型,还能当 NAS、路由器、家庭自动化中枢……
但每当我看着它那方正的金属外壳,总觉得:这样一台有潜力的“小猫”,如果仅仅蜷缩在角落默默运算,实在太可惜了。
于是,一个脑洞渐渐浮现:
如果我能让它“抬起头”,变成一台能与我互动的桌面设备,那它是不是可以从“后台工具”变成一个“生活伙伴”?
这便是“闺蜜机”计划的起点。
开始组装
在真正开搞之前,我先明确了“闺蜜机”的几个核心需求:
颜值在线 —— 看着舒服,不是那种硬件工程师风格的“裸奔主机”。
交互自然 —— 既能远程操作,又能直接触控或观看。
功能丰富 —— 能看剧、听歌、聊天、展示信息、写点代码。
安静节能 —— 长期开机无压力。
于是,我从硬件角度进行了改造规划。
购买支架:从“盒子”到“屏幕”
首先要解决的,就是外观与摆放问题。
我在网上找了一个通用闺蜜机支架,价格大约 100 元出头。直接就可以把带有 VESA 接口的显示器放上去。
2. 接线:极简而有序
接线部分只有三条主线:
电源线:为智慧屏供电;
HDMI 线:连接懒猫微服主机输出视频信号;
USB 线(可选):用于连接外设,比如鼠标或者键盘。
软件篇:赋予智慧的灵魂
硬件准备好之后,就是灵魂的部分:让懒猫“活起来”。
启动智慧屏
开机后,懒猫智慧屏会显示一个简洁的启动界面。
等待片刻,你就能看到主界面上已经安装好的各种应用:懒猫相册,懒猫网盘,懒猫导航等等。
远程访问与控制
因为没有配备实体遥控器,我采用了浏览器远程访问的方式来操作图形界面。
只需在电脑上输入懒猫智慧屏的地址,就能看到相同的桌面画面,并进行鼠标和键盘控制。
这一步的意义很大——意味着你可以:
在笔记本上操作界面;
同时在闺蜜机上显示播放内容;
甚至远程帮家人操作他们的智慧屏。
懒猫系统的远程桌面功能做得相当稳定,延迟极低,堪比本地操作体验。
娱乐体验:智慧屏不只是个显示器
比如在 MoonTV 中搜索“陆小凤传奇”,张智霖太帅了。
播放效果如下图所示,很流畅。在那一刻,几乎会忘记这是一台服务器输出的画面。 现在它更像一台高颜值的桌面智能显示器,可以用来看剧、刷视频、听音乐。
延伸玩法:更多可能
当然,“闺蜜机”的定义不仅仅是娱乐工具。 它更像一个“个人助理终端”,能陪你工作、陪你放松。懒猫微服+智慧屏的组合还能解锁更多玩法:
AI 助理终端:接入语音识别与 ChatGPT API,做成一个真正会说话的桌面 AI。
智能家居中枢:接入米家、Home Assistant,实现语音控制全屋设备。
个人数据中心:利用 Nextcloud 或 Syncthing 实现文件自动同步。
远程监控中心:作为摄像头视频的实时显示终端。
数字相框模式:闲置时自动播放相册、日历、天气与提醒。
从某种意义上说,这台改装“闺蜜机”已经超越了硬件定义,它更像一种新的桌面生活方式。
它是你写代码时的终端,是你放松时的屏幕,是你日常信息流的窗口——
更是一个静静陪在你身边、懂你的“小伙伴”。
当一个小小的懒猫微服,既能陪你写代码,又能陪你看剧、听歌、聊天、思考——它就不再是一台设备,而是一种生活方式的具象化。
从技术角度看,它展示了软硬件结合的极致灵活性;
从生活角度看,它代表了一种新的“桌面哲学”:
技术不只是生产力,它也能成为陪伴力。
有时候我们折腾设备,并不是因为它实用,而是因为那份创造的乐趣。
当一个原本冰冷的硬件,因为你的想法与双手,变得有温度、有灵魂——
那一刻,它就成了你的“闺蜜”。
这次改装只是个开始。
也许不久之后,“闺蜜机”会成为每个桌面爱好者的新宠——
一个能理解你的节奏、陪你共度时光的数字伙伴。
懒猫微服,从此有了另一种人生。
---
## 懒猫微服进阶心得(十四):接入 Casdoor,玩转OpenID Connect(OIDC)
- URL: https://blog.no-claw.com/posts/93c9e4e6/
- 日期: 2025-09-19
- 分类: 懒猫微服 / 进阶
- 标签: 懒猫微服, 单点登录
在之前的文章中,我们演示了如何基于 懒猫自带的 OpenID Connect(OIDC) 来实现身份认证。那属于「平台内置」的简化方案,主要是帮助大家快速理解 OIDC 的基本使用场景。
这一次,我们换一个更通用、更贴近生产实践的方式:使用应用商店里的 OpenID Connect(OIDC) Provider —— Casdoor。Casdoor 是一个开源的统一身份认证平台,支持完整的 OIDC 协议,可以作为独立的 IdP(Identity Provider)对接到任何应用。通过它,我们不仅能跑通最标准的授权码流程,还能深入理解 OIDC 的关键环节:授权跳转、Token 换取、ID Token 验签以及用户信息获取。
我们经常听到 单点登录(SSO)、OAuth2、JWT 这些词。
OIDC(OpenID Connect)正是基于 OAuth2 的标准化身份认证协议。它的核心作用是:
帮助应用确认用户是谁(认证)
不需要你自己维护密码和用户库(交给 IdP)
与 OAuth2 完全兼容,可以同时获取访问 API 的能力(授权)
一个形象的比喻:
OAuth2 提供的是“门禁卡”功能(你能不能进某个房间)
OIDC 在此基础上加了“身份证”功能(你是谁)
OIDC 基本流程
OIDC 的标准授权码流程(Authorization Code Flow):
用户访问应用 → 应用把用户跳转到 IdP 登录页
用户在 IdP 登录 → IdP 返回一个授权码(code)
应用后端用授权码换 token(包括 access_token 和 id_token)
应用验证 id_token → 确认用户身份
可选:调用 userinfo 接口 获取更详细的用户信息
进入管理后台
登录到你的 Casdoor 管理控制台(通常是 https://casdoor..heiyu.space/ 或者部署时设定的管理地址)。
用管理员账号(admin/123)进入后台。
OIDC 应用必须挂在某个 Organization 下。
默认有一个 built-in 组织,可以直接用,也可以新建一个。
创建应用 (Application)
在左侧菜单选择 Application → 点击 Add。
填写基本信息:
Name:应用名称(例如 my-oidc-app)。
Display name:显示名称。
Organization:选择上一步的组织。
Logo:可选。
在 Authentication 部分:
设置 Redirect URIs:
必须和你应用里写的一致,例如:
1
http://localhost:5001/auth/callback
在 OAuth 授权类型 部分:勾选 authorization_code(最标准的流程)。
获取 OIDC 参数
保存后,在应用详情页可以看到:
Client ID
Client Secret
Redirect URI(你填的)
同时,Casdoor 服务提供一个 OIDC Discovery 地址:
1
https:///.well-known/openid-configuration
这个地址返回 JSON,里面包括:
issuer
authorization_endpoint
token_endpoint
userinfo_endpoint
jwks_uri
end_session_endpoint
这些就是在后端应用里要配置的参数。
验证配置
在浏览器里直接访问:
1
https:///.well-known/openid-configuration
如果能看到 JSON,说明 OIDC 服务已开启。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
{
"issuer": "https://casdoor.xxxx.heiyu.space",
"authorization_endpoint": "https://casdoor.xxxx.heiyu.space/login/oauth/authorize",
"token_endpoint": "https://casdoor.xxxx.heiyu.space/api/login/oauth/access_token",
"userinfo_endpoint": "https://casdoor.xxxx.heiyu.space/api/userinfo",
"jwks_uri": "https://casdoor.xxx.heiyu.space/.well-known/jwks",
"introspection_endpoint": "https://casdoor.xxx.heiyu.space/api/login/oauth/introspect",
"response_types_supported": [
"code",
"token",
"id_token",
"code token",
"code id_token",
"token id_token",
"code token id_token",
"none"
],
"response_modes_supported": ["query", "fragment", "login", "code", "link"],
"grant_types_supported": ["password", "authorization_code"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": [
"RS256",
"RS512",
"ES256",
"ES384",
"ES512"
],
"scopes_supported": [
"openid",
"email",
"profile",
"address",
"phone",
"offline_access"
],
"claims_supported": [
"iss",
"ver",
"sub",
"aud",
"iat",
"exp",
"id",
"type",
"displayName",
"avatar",
"permanentAvatar",
"email",
"phone",
"location",
"affiliation",
"title",
"homepage",
"bio",
"tag",
"region",
"language",
"score",
"ranking",
"isOnline",
"isAdmin",
"isForbidden",
"signupApplication",
"ldap"
],
"request_parameter_supported": true,
"request_object_signing_alg_values_supported": ["HS256", "HS384", "HS512"],
"end_session_endpoint": "https://casdoor.xxxxx.heiyu.space/api/logout"
}
用 Postman 或者 oidc-client 测试一下授权流程,看看能不能拿到 code、access_token、id_token。
✅ 至此,Casdoor 端就配置好了。剩下的就是在应用端(RP)写 OIDC 客户端代码
OpenID Connect(OIDC)代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import os, requests, jwt
from urllib.parse import urlencode
from flask import Flask, redirect, request, session, url_for, jsonify, abort
from dotenv import load_dotenv
from jwt import PyJWKClient
# ---------------- Load env ----------------
load_dotenv()
app = Flask(__name__)
app.secret_key = os.getenv("FLASK_SECRET_KEY", "dev-secret")
ISSUER = os.getenv("OIDC_ISSUER")
CLIENT_ID = os.getenv("OIDC_CLIENT_ID")
CLIENT_SECRET = os.getenv("OIDC_CLIENT_SECRET")
REDIRECT_URI = os.getenv("OIDC_REDIRECT_URI")
# ---------------- Discover OIDC endpoints ----------------
discovery = requests.get(f"{ISSUER}/.well-known/openid-configuration").json()
AUTH_ENDPOINT = discovery["authorization_endpoint"]
TOKEN_ENDPOINT = discovery["token_endpoint"]
USERINFO_ENDPOINT = discovery["userinfo_endpoint"]
JWKS_URI = discovery["jwks_uri"]
# ---------------- Routes ----------------
@app.route("/")
def index():
if "user" in session:
return f"Hi, {session['user'].get('name') or session['user']['sub']}
退出"
return "使用 Casdoor 登录"
@app.route("/login")
def login():
params = {
"client_id": CLIENT_ID,
"response_type": "code",
"scope": "openid profile email",
"redirect_uri": REDIRECT_URI,
"state": "xyz123", # 可以生成随机数并存到 session
"nonce": "abc456"
}
return redirect(f"{AUTH_ENDPOINT}?{urlencode(params)}")
@app.route("/auth/callback")
def callback():
if "error" in request.args:
return f"Error: {request.args['error']}"
code = request.args.get("code")
if not code:
abort(400, "Missing code")
# 1. 换取 Token
data = {
"grant_type": "authorization_code",
"code": code,
"redirect_uri": REDIRECT_URI,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
}
token_resp = requests.post(TOKEN_ENDPOINT, data=data).json()
id_token = token_resp.get("id_token")
access_token = token_resp.get("access_token")
# 2. 验证并解码 ID Token
jwks_client = PyJWKClient(JWKS_URI)
signing_key = jwks_client.get_signing_key_from_jwt(id_token)
claims = jwt.decode(
id_token,
signing_key.key,
algorithms=["RS256"],
audience=CLIENT_ID,
issuer=ISSUER,
)
# 3. 获取用户信息
userinfo = requests.get(
USERINFO_ENDPOINT,
headers={"Authorization": f"Bearer {access_token}"}
).json()
session["user"] = {
"sub": claims["sub"],
"name": userinfo.get("name", claims.get("name")),
"email": userinfo.get("email", claims.get("email")),
}
return redirect(url_for("profile"))
@app.route("/profile")
def profile():
if "user" not in session:
return redirect("/")
return jsonify(session["user"])
@app.route("/logout")
def logout():
session.clear()
return redirect("/")
if __name__ == "__main__":
app.run("0.0.0.0", 5001, debug=True)
这个是.env 的环境变量:
1
2
3
4
5
6
FLASK_SECRET_KEY=replace-with-a-random-32-bytes-string
OIDC_ISSUER=https://casdoor.name.heiyu.space
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
OIDC_REDIRECT_URI=http://localhost:5001/auth/callback
下面是代码解读:
Flask 应用 = OIDC 客户端(RP);Casdoor = 身份提供方(IdP)。
/login:把浏览器重定向到 IdP 的 授权端点。
/callback:IdP 回调携带 code → 后端用 code去 令牌端点 换 access_token + id_token → 用 JWKS 公钥校验 id_token → 用 access_token 拉 userinfo。
/profile:展示从 userinfo/ID Token 得到的用户信息。
/logout:清空本地会话。
依赖与配置
1
2
3
4
5
import os, requests, jwt
from urllib.parse import urlencode
from flask import Flask, redirect, request, session, url_for, jsonify, abort
from dotenv import load_dotenv
from jwt import PyJWKClient
requests:调 OIDC 的 HTTP 端点。
PyJWT + cryptography:验证 id_token 的数字签名。
PyJWKClient:根据 JWT 头里的 kid,自动从 jwks_uri 拉对应公钥。
urlencode:把授权请求的参数拼到 URL 上(避免手写字符串拼接出错)。
1
2
3
4
5
6
7
8
load_dotenv()
app = Flask(__name__)
app.secret_key = os.getenv("FLASK_SECRET_KEY", "dev-secret")
ISSUER = os.getenv("OIDC_ISSUER")
CLIENT_ID = os.getenv("OIDC_CLIENT_ID")
CLIENT_SECRET = os.getenv("OIDC_CLIENT_SECRET")
REDIRECT_URI = os.getenv("OIDC_REDIRECT_URI")
从 .env 读取 Issuer / Client / Secret / Redirect URI,和 Casdoor 后台登记的必须完全一致(协议、域名、端口、路径一字不差)。
通过 Discovery 自动找端点
1
2
3
4
5
discovery = requests.get(f"{ISSUER}/.well-known/openid-configuration").json()
AUTH_EP = discovery["authorization_endpoint"]
TOKEN_EP = discovery["token_endpoint"]
USERINFO_EP = discovery["userinfo_endpoint"]
JWKS_URI = discovery["jwks_uri"]
OIDC 规范要求 IdP 暴露发现文档,里面告诉你:授权端点、令牌端点、用户信息端点、JWKS 公钥地址等。
这么做的好处:不写死 URL,换 IdP/升级版本也不怕路径差异。
首页 & 登录
1
2
3
4
5
@app.route("/")
def index():
if "user" in session:
return f"欢迎 {session['user'].get('name') or session['user']['sub']}
退出"
return "使用 Casdoor 登录"
简单展示:有会话就显示用户名,否则给一个“登录”链接。
1
2
3
4
5
6
7
8
9
10
11
@app.route("/login")
def login():
params = {
"client_id": CLIENT_ID,
"response_type": "code", # 标准授权码流程
"scope": "openid profile email", # 至少要有 openid;加 profile/email 便于拿到名字/邮箱
"redirect_uri": REDIRECT_URI,
"state": "xyz123", # 防 CSRF(下面会给“随机+校验”的升级版)
"nonce": "abc456" # 防重放(也建议随机+校验)
}
return redirect(f"{AUTH_EP}?{urlencode(params)}")
state:浏览器去 IdP 再回来时要原样带回;用于确认这真是你发起的请求(防 CSRF)。
nonce:IdP 会把它放进 id_token,回调后核对一致(防重放/混淆响应)。
Demo 为了短小,先写了固定值。写文章时要强调:生产必须随机、并在回调里校验。
回调:换令牌 → 验签 ID Token → 拉用户信息
1
2
3
4
5
6
7
@app.route("/auth/callback")
def callback():
if "error" in request.args:
return f"Error: {request.args['error']}"
code = request.args.get("code")
if not code: abort(400, "Missing code")
IdP 会带着 ?code=...&state=... 回来。这里先取出 code,并处理错误场景(用户取消授权等)。
1
2
3
4
5
6
7
8
9
10
11
# 1) 用 code 换 token
token_resp = requests.post(TOKEN_EP, data={
"grant_type": "authorization_code",
"code": code,
"redirect_uri": REDIRECT_URI,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
}).json()
id_token = token_resp.get("id_token")
access_token = token_resp.get("access_token")
这一步是服务端到 IdP的 POST。两种常见做法:
把 client_id/client_secret 放表单(本例);
或用 HTTP Basic(IdP 要求不同,文章里可顺带提一嘴)。
返回里关键是 id_token(JWT,证明“你是谁”)和 access_token(调用 userinfo 的 Bearer Token)。
1
2
3
4
5
6
7
8
9
10
# 2) 验证 id_token(必须做)
jwks_client = PyJWKClient(JWKS_URI)
signing_key = jwks_client.get_signing_key_from_jwt(id_token)
claims = jwt.decode(
id_token,
signing_key.key,
algorithms=["RS256"], # Casdoor 也可能配 ES256/ECDSA,按实际配置来
audience=CLIENT_ID, # aud 必须包含你的 client_id
issuer=ISSUER, # iss 必须等于你的 Issuer
)
为什么一定要验签?否则任何人都可以伪造一个 “自称由 IdP 签发”的 JWT。
PyJWKClient 会读 JWT 头部的 kid,去 JWKS_URI 拉这把钥匙的公钥。
同时校验标准字段:iss/aud/exp/iat/...。如果你还用了 nonce,建议对 claims["nonce"] 做一致性校验。
1
2
3
4
5
# 3) 拉取用户信息
userinfo = requests.get(
USERINFO_EP,
headers={"Authorization": f"Bearer {access_token}"}
).json()
不是所有 IdP 都在 id_token 里带全资料,所以通常再拉一次 userinfo。
这一步需要前面的 access_token。
1
2
3
4
5
6
7
# 4) 缩小会话,只存最小字段(避免 Cookie > 4KB)
session["user"] = {
"sub": claims["sub"], # 唯一ID
"name": userinfo.get("name", claims.get("name")), # 没有就退回到ID Token
"email": userinfo.get("email", claims.get("email")),
}
return redirect(url_for("profile"))
强烈建议:只把少量字段放进 session(Flask 默认把 session 放加密 Cookie,4KB 有上限)。不要把整个 token/JWT/raw 塞进去。
查看资料 & 退出
1
2
3
4
@app.route("/profile")
def profile():
if "user" not in session: return redirect("/")
return jsonify(session["user"])
只从会话里取“精简后的用户档案”返回给前端。
1
2
3
4
@app.route("/logout")
def logout():
session.clear()
return redirect("/")
清本地会话即可。若要单点登出,可以再调用 IdP 的 end_session_endpoint(用 Discovery 取到)并带 post_logout_redirect_uri。
小结:懒猫微服 + Casdoor OIDC
通过这次实战,我们完成了一个从 Casdoor 配置 到 应用端代码实现 的 OIDC 流程。
这说明在懒猫微服中,大家不仅可以直接用内置的 OIDC 功能,还可以自由选择商店里的 OIDC Provider(如 Casdoor) 来扩展身份认证能力。
懒猫微服不仅能用自带的 OIDC,更能灵活调用商店里的 Casdoor 等 Provider,满足更灵活的认证与单点登录需求。
---
## 懒猫微服故事篇(四):不折腾内网穿透了,懒猫微服一站式全搞定!
- URL: https://blog.no-claw.com/posts/9806a8cb/
- 日期: 2025-09-18
- 分类: 懒猫微服 / 故事
- 标签: 懒猫微服, NAS
作为一个开发人员,可谓在服务器环境上操碎了心。除了环境搭建,最头疼的就是远程访问与内网穿透的问题。
这些年来,我更换了一代又一代的工具与方案,也见证了从“草台班子”到“优雅简洁”的进化过程。
虽然现在有了 懒猫微服 的客户端就能直接解决,只需要在手机或者电脑上安装客户端,就可以无缝连回家里,自动切换内外网,无需额外配置。回头看看这一路的折腾,依然让人感慨良多。决定记录下来。
阶段一:初出茅庐 —— TeamViewer / ToDesk
最早接触远程桌面,大多数人用的就是 TeamViewer、ToDesk 这类软件。
它们的优点是:安装简单、即开即用,不需要额外配置公网。对于刚入行的新人来说,能在办公室远程操控家里的电脑,那就是神技。
但是,缺点也很明显:
只能图形连接:延迟较大,对命令行和运维操作带来额外开销。
速度受限:免费版带宽瓶颈明显,卡顿严重,体验不佳。
安全性存疑:所有数据都走第三方服务器,中途链路不透明。
出海限制:连接海外设备时经常掉线,稳定性不足。
可以说,这个阶段只是“远程控制的启蒙”。
阶段二:崭露头角 —— 花生壳
花生壳 算是让我第一次接触到了“内网穿透”这个的启蒙开始。
他的出现,让我意识到:
原来可以不用公网 IP
通过 NAT 穿透技术,也能让外网访问到内网服务
那时用花生壳搭建博客、Web 服务,甚至把实验室的算力卡、NAS 暴露到外网,已经让人兴奋不已。
遗憾的是,免费额度有限,带宽不高,延迟非常大,有时候甚至打洞不成功。
虽然限制不少,但花生壳无疑为内网穿透的发展“埋下了种子”。
阶段三:锋芒渐露 —— 云服务器搭建 FRP
后来在晚上搜索方案。得知了一个叫做FRP(Fast Reverse Proxy)的项目。
后来我选择在阿里云学生机上自建 FRP,实现公网访问。
这一阶段的特点是:
灵活性大幅提升:可以自定义端口、协议,搭建专属穿透服务。
更贴近开发者:可以自由配置内外网服务。
但问题也随之而来:
配置文件相对繁琐
维护成本高,不论是人力还是云服务器的费用是一笔不小的开销。
阿里云基础服务器带宽很少,经常会造成访问延迟。
阶段四:乘风破浪 —— 蒲公英盒子
一次偶然的网上冲浪,看见 B 站 UP 主推荐蒲公英盒子,于是下单购买了一个。蒲公英盒子 把穿透功能做成了“小黑盒”,即插即用,免去复杂配置。
它的优势是:
傻瓜化:插上网线就能用,零门槛。
稳定性:设备常驻,后台运维稳定。
适合小微团队:旁路由无缝侵入,智能转发流量。
但它也有局限:
因为是小黑盒,所以很多技术细节不透明
穿透的带宽有限制
售后体验非常不好
所以最后我弃用了这个方案。
阶段五:披荆斩棘 —— 公网 IP
联通还能给动态的公网 IP,所以可以在路由器上开端口转发了。
公网 IP 的优势不言而喻:
原生直连,没有中间转发
速度快,延迟低
自由度高,怎么玩都行
但问题是:
家庭宽带公网 IP 封禁常用端口(80/8080/443)
运营商偶尔没有缘由的封禁端口
安全风险极高,直接暴露在公网上,没有任何防火墙和防止 DDNS 的措施
所以,公网 IP 是一个选择,但是并不是最终的方案。
某年某月,外出一段时间,于是用树莓派做了一个监控放在家里推流,然后通过 http 暴露在互联网上,但是没过几天就被封端口了。回来检查内网一切正常。
终极大法:未来可期 —— 懒猫微服
直到朋友给我推荐了 懒猫微服,这条漫长的折腾曲线才真正画上句号。
它带来了超稳定的内网穿透体验,让我无论身处何地,都能丝滑访问家里的设备。
硬件级双重验证,彻底杜绝黑客攻击;一线工程师贴心支持,让人不再为复杂配置而焦虑。
几乎开箱即用,自动识别并切换内外网,真正做到零额外配置。
下面这张测速图,是我在旅游途中、相距家中千里之外时的真实测得。懒猫微服本身不做任何限速,访问体验就像设备就在身边,再也不用为防火墙、端口映射这些恼人的问题烦恼。
我现在的常用使用场景如下:
通过客户端 APP 直接访问微服网盘或者商店 APP
通过懒猫转发访问 windows(RDP)
通过懒猫转发访问 windows(SSH)
通过懒猫转发访问 KVM over IP(webrtc)
通过懒猫转发访问其他 NAS(Snology,QNAP)
通过懒猫做旁路由直接通过内网访问其他设备
两个安装懒猫微服的 APP 互相访问
网络入口可以选择不同的颗粒度,微服所在局域网(物理网卡),容器内(虚拟网桥) ,登陆微服的客户端或者特定的客户端,或者包括前面所有的规则。
出口也可以选择不同的应用,客户端,甚至是没有登陆微服的客户端,只要网络可达,都可以做流量转发,实属一个居家必备的网关。
为什么它是终极解法?
它几乎集合了之前所有阶段的优点:
免公网 IP:自动穿透,省钱省心
速度稳定:直接走互联网,延迟低
安全加固:硬件双重验证,杜绝黑客攻击
全场景覆盖:桌面、SSH、数据库、NAS、开发环境一个客户端搞定
即插即用:媲美蒲公英盒子,但更灵活开放
对我来说:
不再为端口映射、防火墙折腾
不再为公网 IP 掏冤枉钱
不再担心安全风险
只需一个懒猫微服,就能把 远程运维、开发调试、家庭娱乐 全部串联起来。
从 TeamViewer → 花生壳 → FRP → 蒲公英 → 公网 IP,
这是我的的远程访问修炼史,也是一路的折腾与妥协。
如今,懒猫微服成了真正的终结者。远程访问不再是负担,而是一件轻松、省心的事。
未来可期,组网无忧。
---
## 一根网线搞定远程运维,GL-RM1PE 深度体验:远程运维、装机、开机一体化的 KVM over IP
- URL: https://blog.no-claw.com/posts/63cca403/
- 日期: 2025-09-15
- 分类: 电子产品 / 外设
- 标签: 外设, KVM, POE
一直以来,我都很喜欢 Gl.iNet 的产品。它们通常设计简洁、功能实用,既能满足极客和开发者的需求,又能兼顾家庭用户的体验。无论是路由器还是 KVM 远程管理器,Gl.iNet 在“小而美”的产品线上一直有稳定发挥。
这并不是一台路由器或者 NAS,而是一款 KVM over IP 设备。在传统企业机房里,这类设备往往被用来实现服务器的远程管理:即便服务器关机、系统崩溃,管理员依然可以通过远程方式查看屏幕、控制键鼠,甚至重装系统。过去,这样的功能往往属于 Dell iDRAC、HP iLO、Supermicro IPMI 等服务器管理模块的专属领域。但现在,Gl.iNet 把它做成了一台独立设备,价格和使用门槛也被大大降低,让普通运维人员和 Homelab 爱好者也能体验到“企业级远程管理”的能力。
换句话说,如果你想远程操作一台服务器,哪怕它彻底关机、哪怕系统崩溃,甚至你要给它重装系统,这个小小的设备都能帮你完成。这类设备过去通常是企业机房里的“高端选配”,但现在,Gl.iNet 把它带到了大众可用的价位。
背景与定位:KVM over IP 为什么重要?
在进入具体体验之前,我们先谈一谈 为什么 KVM over IP 设备重要。
传统的远程管理方式,大体可以分为两类:
软件级远程控制:比如 VNC、XRDP、TeamViewer、AnyDesk。这类方案依赖目标系统正常启动、网络正常可用,才能实现远程桌面。如果系统崩溃或者驱动异常,远程就失效了。
硬件级远程管理:比如服务器厂商自带的 iDRAC、iLO,或者独立的 KVM over IP。它们直接劫持视频信号和输入设备,不依赖系统和驱动,即便设备黑屏、关机,也能操作。
这就好比一台电脑出故障,软件方案只能“看得到桌面才管用”,而硬件方案则能“从按电源键那一刻就开始管理”。对于机房里的运维人员,这几乎是救命的功能。
过去个人用户或者中小企业要实现这种能力,往往需要购买昂贵的服务器管理卡。但如今,Gl.iNet 通过 GL-RM1PE 让这种能力变得可负担,也让 Homelab 用户有机会体验到企业级的远程管理。
产品简介:KVM + PoE = 极简方案
GL-RM1PE 的设计理念用一句话概括就是:一根网线,搞定一切。
它把三件事情整合到了一根网线上:
供电:支持 PoE(Power over Ethernet),无需额外电源适配器。
网络:千兆网口即插即用,自动联网。
远程控制:完整的 KVM 功能,覆盖键盘、视频、鼠标。
这种“单线极简”的设计,在机房和家庭实验室环境中非常有价值。机房里本来布线就复杂,减少一根电源线就意味着更高的整洁度和更低的维护成本。在家里,少一根插座电源适配器,也能让桌面和机柜更干净。
接口布局方面,设备正面依次是:
USB 2.0 接口:用于连接手指机器人或其他外设
HDMI 接口:采集被控设备的输出
USB 控制接口:模拟键盘鼠标输入
RJ45 网口:千兆接口,支持 PoE
侧面还有一个 Type-C 供电口,在没有 PoE 交换机的情况下,可以用外接电源。整体来看,它就像是在任何 PC/服务器上外挂了一个“远程管理模块”。
手指机器人:物理开机的最后一环
KVM 能解决大部分远程管理问题,但仍有一个关键问题:如果设备彻底关机怎么办?
这就是官方配套的 手指机器人 登场的地方。
顾名思义,它就是一个能模拟“手指按电源键”的小机械装置。
它的设计要点是:
模拟物理按键:通过小机械臂实际按下电源键
粘贴式安装:无需拆机或改造,直接贴在电源键上方
CR2 电池供电:续航超过一年,电池可更换
USB 蓝牙接收器:插在 KVM 设备上,与手指机器人配对
这样,即便远程主机彻底关机,你依然可以通过 KVM 下达指令,让手指机器人去“按电源键”。
我亲测的体验是:在管理后台端点击按钮,机柜里的手指机器人立刻按下动作,把服务器开机了。这种感觉很神奇,就像你拥有了一只可以随时帮你“按开机键”的远程手。对于远程运维人员来说,这意味着再也不用担心“系统挂掉后没人帮忙按电源键”的尴尬。
从外观来看,产品正面有 GL.iNet 的 logo 和 LED 指示灯,铝制外壳配合银灰色表面处理,质感相当不错,放在机柜里也很协调。
在机柜里的实际使用效果如下:
接口从左到右依次是:
USB 2.0 接口:连接手指机器人
HDMI 接口:采集被控设备输出
USB 控制接口:模拟键盘和鼠标
RJ45 网口:支持 POE
侧面还预留了 Type-C 供电口,如果你没有 POE 交换机,可以用外接电源。
拆开手指机器人的外壳,可以看到一个 USB 接收器和一颗 CR2 电池。电池可替换,维护成本不高。
网络与接入:mDNS + HTTPS 的细节优化
我家里的网络架构是中兴晴天的 AC+AP,完全使用 POE 供电,因此 GL-RM1PE 可以直接插在 AC 上使用(装修时预留了网口)。
接上 POE 之后,设备自动上线,可以通过路由器后台查看 IP。更方便的是,它支持 mDNS,直接访问:
1
https://glkvm.local/#/
就能进入后台管理页面。即使你访问的是 80 端口,那么也会被重定向到 https 的 443 端口,所以这样访问也可以。
1
glkvm.local
还可以通过 ping 查找设备 IP:
1
2
3
4
5
ping glkvm.local
PING glkvm.local (192.168.x.x): 56 data bytes
64 bytes from 192.168.x.x: icmp_seq=0 ttl=64 time=4.539 ms
64 bytes from 192.168.x.x: icmp_seq=1 ttl=64 time=4.180 ms
64 bytes from 192.168.x.x: icmp_seq=2 ttl=64 time=4.200 ms
内网体验:比 VNC 更流畅的 WebRTC
Web 管理界面是它的主要入口。
初次访问需设置管理员密码
后续输入密码即可直连,无需复杂认证流程
在内网环境下,即便是 KDE 桌面这种对图形要求比较高的环境,WebRTC 的画面流畅度和延迟表现也明显优于 VNC 和 XRDP。以前用 VNC,常常有卡顿和延迟,键盘输入延时明显;而在 GL-RM1PE 上,鼠标和键盘几乎是“秒响应”,就像本地操作一样。
它不仅解决了延迟问题,还提供了专业的视频设置:
支持 H.264 编解码
最高可达 4K 输出
支持分辨率 EDID 设置和画面旋转
无论是普通显示器还是特殊比例的屏幕,都能轻松适配。
声音和输入设备也做得很细致:
扬声器和麦克风:可远程传输音频,延迟很低
键盘:支持虚拟键盘输入,触控友好
鼠标:支持光标显示、防休眠抖动、滚轮速率调整、正反滚动切换
此外,它还内置了 剪切板共享、快捷键模拟(如 Ctrl+Alt+Del)、Wake-on-LAN、内置终端 等功能。这些都大幅提升了远程操作的便利性,让体验接近“本地化”。
特别的是,KVM 直接采集 HDMI 输出,即便设备锁屏或休眠,也能远程操作。
虚拟媒体与 ISO 挂载:远程装机神器
远程运维里,最棘手的情况之一是 系统需要重装。如果没有人现场插入 U 盘或者光盘,基本无法操作。而 GL-RM1PE 提供的 虚拟媒体挂载 功能,几乎完美解决了这一问题。
这个非常实用的功能是 虚拟媒体挂载。GL-RM1PE 提供的 虚拟媒体挂载 功能简直是神器。GL-RM1PE 内置 32G eMMC,可以存放主流操作系统的 ISO 镜像。在 Web 界面即可上传 ISO,然后将其作为虚拟光驱挂载。重装系统、文件传输都非常方便。
对于远程运维人员来说,这个功能意味着:你可以在千里之外,帮一台死机的服务器重装系统,而不需要有人在现场插 U 盘。
我在 EndeavourOS x86_64 上成功挂载后,能够看到 PopOS 镜像已经挂载。
设备提供了两种方式:
文件挂载:将 eMMC 当作光驱,从系统内部传输文件。
镜像挂载:把 ISO 当作系统盘引导,用于安装新系统。
这个是在 BIOS 中读到的引导项,Gl.iNet Flash Drive 就是这个 GL-RM1PE 的镜像引导,和使用 CD 或者刻录的 U 旁随身碟完全一致。选择 Gl.iNet 的引导之后就进入了我们熟悉的装机环节。
云服务集成与公网访问:在哪里都能访问
本地用得好,但公网场景怎么办?
Gl.iNet 直接在产品里送了一个 终身免费的内网穿透服务。
电脑端可用 PC 客户端
手机/iPad 可直接访问 https://www.glkvm.cn
这个网址来访问。即使在公网环境下,也能远程访问设备。
因为官方提供终身了内网穿透的功能,所以这是一个买硬件送穿透服务的。
这等于硬件买了之后,官方帮你免去了搭建反向代理、DDNS 的折腾,性价比更高。
使用云服务的话,需要注册账户,然后每次需要登录这个账号。如果你有特殊的安全需要,还可以设置二次验证的 MFA。
设备绑定完成后会显示在列表中,点击远程控制时需重新输入密码,以确保远程管理过程中的安全可靠。
更强的是,它还能接入 Tailscale,加入私有组网,这样能够更加方便的进行异地组网。
如果你之前使用过 Tailscale 的话,那么只需要点点鼠标就能轻松加入之前的组里面。
我把 Macbook,群晖和 GL-RM1PE 放在了一个虚拟局域网里面,这样即使没有公网 IP 也能直接互联。
隐藏玩法:小型 Linux 主机
体验过程中我发现,GL-RM1PE 本身就是一台小型 Linux 机器:
CPU:4 核
内存:1G
存储:32G eMMC
环境:BusyBox
GLKVM 同时还支持终端访问,甚至可以看到是 BusyBox 的环境,这个对于开发者十分友好,甚至还可以 ssh 到内网的 Linux 机器。而且我们也能够看到,有线网卡的协商速率是 1000Mbps。
在 htop 里能看到完整的 4C1G 配置,用来跑 WebRTC 推流完全足够。
机器提供了 32G 的 eMMC,挂载在 /userdata/media,能存放系统 ISO 或作为文件服务器使用。
也就是说可以把这个硬件当作一台小型 Linux 服务器来使用,比如执行python -m http.server可以开启一个文件服务器,当然,BusyBox 环境不支持包管理和 Docker,但作为应急环境、调试环境,已经非常有价值。
竞品与行业对比:和 iDRAC、iLO、TeamViewer 的区别
如果把 GL-RM1PE 放到整个远程管理市场中,它的定位非常独特。
相比软件远程(VNC、XRDP、TeamViewer、AnyDesk):
优点:不依赖操作系统,哪怕系统崩溃也能操作;延迟更低。
缺点:需要额外硬件。
相比服务器管理卡(Dell iDRAC、HP iLO、Supermicro IPMI):
优点:价格低得多,不绑定服务器厂商,通用性强。
缺点:功能上略简单,比如缺少企业级的监控和批量管理功能。
对于个人和中小企业来说,GL-RM1PE 正好填补了中间空白:比软件方案更可靠,比企业级方案更便宜。
总结:远程运维的“全能工具箱”
体验下来,我认为 GL-RM1PE + 手指机器人 是一套完整的远程管理解决方案。它把过去属于企业级服务器的功能,带到了个人和中小企业可用的价位。
优势总结:
一根网线搞定供电 + 网络
WebRTC 推流低延迟,优于 VNC/XRDP
内置 32G eMMC,支持虚拟媒体挂载,远程装机更轻松
手指机器人解决远程物理开机痛点
内网直连 + 官方云服务双保险
支持 Tailscale,扩展性强
自身就是一台小型 Linux 主机,调试灵活
无论是 IT 运维工程师,还是喜欢折腾的 开发者与 Homelab 爱好者,这套组合都非常值得一试。它不是冰冷的“机房专属”设备,而是把远程管理做到了极客友好、开箱即用。同时也期待 WIFI 版本的 KVM 远程控制器早日上市,这样就更加方便了。
---
## 懒猫微服故事篇(三):朋友拿着一台没公网IP的服务器跑来找我,,问我能不能转发?
- URL: https://blog.no-claw.com/posts/52d0811e/
- 日期: 2025-09-13
- 分类: 懒猫微服 / 故事
- 标签: 懒猫微服, NAS
前几天,一个朋友来找我。他说自己新买了一台云服务器,却发现厂商没有分配公网 IP。最关键的是,他已经在上面部署好了服务,还急着要对外共享。于是他问我:
“这台没有公网 IP 的云服务器,还有办法让别人访问吗?”
这其实是很多人都会遇到的情况:
有些云厂商默认不给公网 IP,只能通过内网访问。
公网 IP 要额外收费,而且价格往往不低。
部分地区公网 IP 甚至直接被封禁。
轻量机、学生机这类实验主机,干脆就不提供公网 IP。
那么,有没有办法“曲线救国”?
答案是:当然有。靠的就是 懒猫微服的端口转发功能。
它能让一台没有公网 IP 的云服务器,重新获得对外访问的能力,甚至还能跨懒猫账号共享。
我把这种玩法类比于 HTTPS 的证书卸载,称之为 “登录卸载”。
1. 解决思路
朋友的云服务器上跑着一个 Web 服务,但因为没有公网 IP,只能在内网访问。
我的解决方案是:
在这台云服务器上安装懒猫微服客户端;
找一台有公网 IP 的懒猫微服节点,作为“出口”;
把云服务器的服务端口转发到出口节点上。
这样一来,即便服务器本身只有内网地址,也能借助另一台机器的公网出口,对外提供服务。
此时,只需要在这台服务器上安装懒猫微服客户端,然后记住它的机器名称。
2. 设置端口转发
出于数据安全的考虑,我给他单独开了一个子账号。
因为当转发目标选择“微服客户端”时,只能获取到登录账号下的设备。这也意味着我无法直接读取他机器的域名,更不能帮他添加转发规则,所以需要让他自己登录来配置。
在懒猫微服的控制台里,他只需要新建一个端口转发规则:
转发类型:选择 “微服客户端”,对应的就是那台没有公网 IP 的云服务器;
端口:填写要转发的服务端口(比如 8080);
出口类型:选择 0.0.0.0,绑定到有公网 IP 的微服网卡。
完成后,这台云服务器的服务就顺利“挂”在了公网出口上。
3. 转发测试
配置好后,我先在内网直接用 telnet 验证:
1
2
3
4
telnet a.b.c.d
Trying x.x.x.x...
Connected to x.x.x.x.
Escape character is '^]'.
可以看到,不登录的情况下直接访问懒猫的私有 IP 就能通。
我自己家里有公网 IP,于是只需要在路由器上再做一个转发,就能让这台云服务器的服务真正跑到公网去。
当他看到成功连上之后,才彻底放心:
原来真的能让一台没有公网 IP 的云服务器,也能被公网访问!
为了更方便,我还配合了 DDNS-go 使用。这样即便出口 IP 改变,也能通过域名自动解析到最新 IP,保证随时可访问。
4. 最终效果
经过这套操作,这台“没有公网 IP 的云服务器”,就像普通云主机一样,可以被外网直接访问了。
关键是:全程不需要购买昂贵的公网 IP,配置好之后也不必频繁登录管理,真正做到一次设置,长期使用。
现在网站的流量数据已经很好看了:
小结
传统做法:必须购买公网 IP → 成本高、限制多;
懒猫微服做法:通过端口转发 → 云服务器无需公网 IP,也能轻松对外服务;
额外优势:支持跨懒猫账号共享,配置好之后,朋友也能直接访问。
一句话总结:
有了懒猫微服 + DDNS-go,再“抠门”的云服务器,也能摇身一变,获得公网入口。
---
## 群晖如何配置 Server 酱 Webhook 事件通知?
- URL: https://blog.no-claw.com/posts/e70dd7d7/
- 日期: 2025-09-13
- 分类: 电子产品
- 标签: NAS, 群晖, 家庭网络
网上冲浪的时候看到给群晖配置 server 酱通知的帖子,不过看着比较老旧了。所以自己探索了一下,顺便更新一下教程。也省着下次被停电还得翻日志才能知道。
1. 开启 Webhook 通知
进入 控制面板 → 通知设置,在通知服务里选择 Webhook。这样群晖就能通过 Webhook 推送消息到 Server 酱。
2. 配置 Webhook 提供商
选择 自定义提供商,规则我这里选择了 监听全部,这样所有通知都会转发。
这里的“提供商”和“主题”随便写,自己能认出来就行。真正关键的是 URL 和请求体。
Server 酱的接口地址是:
1
https://sctapi.ftqq.com/.send
保存后群晖会自动在后面拼接一段:
1
text=%40%40TEXT%40%40
这里的 @@TEXT@@ 就是一个占位符,表示群晖实际推送消息时会自动替换成通知内容。
3. 修改请求体
这里是 webhook 的重头戏,所谓 webhook 其实就是 app 里预留了一个 POST API,规定好请求体,然后给用户自由发消息的权利。server 酱接受 title 和 desp 两个字段,无论是 parms 或者 body 都接受。
所以我在群晖 Webhook 的请求体里添加了对应的 JSON:
4. 验证效果
配置完成后,随便触发一条系统通知,就能看到 Server 酱成功收到了推送:
5. 小结
整个配置思路其实很简单:
群晖支持 Webhook 通知;
Server 酱提供了一个接收消息的 API;
我们只需要把群晖的通知格式改成 Server 酱要求的字段,就能打通。
这样一来,无论是停电、磁盘告警,还是系统更新提醒,群晖的消息都能第一时间通过 Server 酱推送到手机上。
---
## 懒猫微服实战入门(二十五):有了懒猫微服之后,我用 Vaultwarden 平替了 1Password订阅,附送迁移方案
- URL: https://blog.no-claw.com/posts/f46ed8e7/
- 日期: 2025-09-10
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服, Vaultwarden, 1Password
之前用的 1Password 会员快到期了,价格也不便宜。正好在懒猫微服商店看到了 Vaultwarden,就顺势换成了这个“平替”。
相信很多人听过 Bitwarden。而 Vaultwarden 是社区开发的 Bitwarden 服务端的轻量级实现,最初叫 bitwarden_rs,后来改名为 Vaultwarden。它用 Rust 编写,资源占用小,非常适合个人用户或者轻量自建场景(树莓派、低配 VPS 等)。它是社区实现的轻量替代版,完全兼容 Bitwarden 的官方客户端,部署成本低,特别适合个人或小团队自建。
在懒猫商店可以直接下载这个应用,安装和使用指南可以参考社区的帖子:玩机攻略:一个开源的密码管理器 Vaultwarden
从 1Password 迁移到 Vaultwarden
我在 Bitwarden 的 Wiki 中找到了一份迁移指南:先从 1Password 导出数据,再在 Vaultwarden/Bitwarden 中完成导入。
在 1Password 中导出:文件 -> 导出 -> 项目名字。我这里选择导出 1PUX 格式的文件,存到本地。这样之前保存的密码、SSH Key 等信息都可以完整导出。
然后在 Vaultwarden 网页端导入刚才的 1PUX 文件(需要提前新建一个文件夹)。
操作很简单,导入后数据就都同步过来了。
客户端体验
如果习惯使用客户端,也可以直接在 App Store 下载桌面端。
除了 MacOS,Vaultwarden 同样支持 Windows 和 Linux。
在客户端的最下面可以切换服务器地址,我这里填的是:https://vaultwarden.name.heiyu.space/。
这样在 APP 里就能直接看到刚刚导入的数据,不需要再手动复制粘贴。
浏览器插件
除了 APP,还可以使用浏览器插件,Vaultwarden 支持了大多数主流浏览器。
我平时用 Chrome 最多,所以直接在 Chrome 商店下载了插件。
和桌面客户端一样,需要先设置好自己部署的服务器地址。
登录流程和桌面端一致。
自动填充与使用体验
我在设置里打开了自动填充功能,这样日常使用方便了不少。
例如登录路由器后台时,Vaultwarden 就能自动填充密码,点击登录即可。
总结
整体体验下来,Vaultwarden 已经完全替代了我之前的 1Password:功能齐全、部署轻量,还能节省一笔订阅费用。对于个人或小团队来说,确实是一个非常值得的平替方案。
---
## 早上起来,Chrome 白屏了……
- URL: https://blog.no-claw.com/posts/a3db028b/
- 日期: 2025-09-07
- 分类: 电子产品 / 电脑
- 标签: 软件
今天一早打开电脑,习惯性点开 Chrome,结果迎面而来的是一片空白。
不是网络卡顿的那种,而是整个浏览器彻底“黑化”:
网页打不开
设置页一片白
就连 F12 的开发者工具也完全空白
当时整个人都愣住了,Chrome 好像一夜之间“失魂落魄”。
怀疑的方向
重启浏览器、重启电脑都没用。只好先切到 Safari 搜了下,结合我前几天的操作,很快锁定了问题。
几天前为了玩《植物大战僵尸 2》网页版,我调整过 Chrome 的 ANGLE 渲染引擎。没想到新版 Chrome 对这个配置并不兼容,直接导致渲染器初始化失败,从而整机白屏。
临时解救办法
既然问题出在 GPU 渲染,那就先让 Chrome 绕开 GPU,加速器禁用掉再说。
在 macOS 上,可以用命令行启动:
常规方式:
1
open -a "Google Chrome" --args --disable-gpu
绝对路径方式:
1
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --disable-gpu
这样 Chrome 会强制禁用 GPU,页面就能正常显示了。
根治步骤
进入浏览器后,把图形渲染器改回默认模式,再重启 Chrome,一切恢复正常。
---
## 米其林 * 黑珍珠 :金陵饭店 - 梅苑 用餐体验
- URL: https://blog.no-claw.com/posts/c6f5f74f/
- 日期: 2025-09-06
- 分类: 零碎生活 / 吃货风云
去南京玩的时候,找酒店老板推荐一些正宗的淮扬菜。由于小厨娘在北京都吃过了,所以他给我推荐了三家本地的黑珍珠。金陵饭店坐落于繁华的新街口,地铁口出去很明显就能看到,据说梅苑一直是南京人心中的白月光。现在也是我的淮扬菜白月光了。
总体来说,梅苑对一人食很友好,很多菜可以点半份,也只收半份的钱,相对来说算是比较轻奢。但是,想想疫情那阵子馋了四季民福的烤鸭,半只也要 150 元(还吃不饱)。那么梅苑的性价比一定比这个要高。
首先门口摆放的就是米其林和黑珍珠的认证,首先这个仪式感就拉满了。没想到第一次黑珍珠给了南京。
整体装修偏古风,可能也是老店的原因吧。并不像是现在一些网红店很华丽的装修。总体是重剑无锋的路线,用菜品说话。东西是好东西,都说他家不怎么做营销。虽然我也反对那种给鸭子听音乐喂牛奶式的广告。金陵饭店目前靠口口相传也不错了。
第一天点的半份金陵烤鸭,没想到在南京这个菜属于凉菜。在北京经常吃小厨娘的烤鸭,现在想来有些不正宗了。因为它确实是烤制,然后放在砂锅里。属于在朱棣之后有改良了一次做法。而我还照猫画虎学着美食博主老高给饭店建议 – 这个热菜一定要把容器加热,这样吃到最后才不会有腥味。。。虽然饭店照做了,但是从南京回来,感觉它那个在分歧的路上越走越远了。倒是梅苑这种蘸着调味汁的方式更加好吃。别看它分量小,但是很扎实。这一顿还吃得饱。
鸭血粉丝汤,这一碗 39 元。这肯定是真的鸭血了。能少见的感觉到对鲜血的渴望。豆腐泡和鸭肠这种配菜都很新鲜,粉丝也入口即化。相比之下,不管北京的还是夫子庙的小厨娘都差了很多。江苏这边的鸭血粉丝汤都是大碗的,和北京的小碗不同,所以甚至感觉黑珍珠的价格甚至要比北京的很多商场性价比要高一些,食材更新鲜一些,烹饪手法更好一些。
第一顿整体的样貌,然后被本地人吐槽没吃到精髓。丢美食博主的脸了。
白色的公筷上写着金陵饭店。
隔了一天去二刷饭店。都说盐水鸭是精髓,所以特意点了一份,也带了一份在冯记买的盐水鸭进行对比。虽然说在冯记买的已经很好吃了,但还要说一说区别。首先从鸭肉的品质上讲,梅苑要更胜一筹,但冯记也很好,这绝不是四季民福和地毯烤鸭的区别,而是一个肉质紧实而有弹性,一个香腻而软烂。这似乎在印证没有一个鸭子能够飞出南京的玩笑。
盐水鸭用的应该是几十年的卤水,然后就很容易联想到李碧华《吃卤水鹅的女人》里面的桥段。越是老卤则味道越浓厚。
那是一大桶半人高,浸淫过数十万只鹅,乌黑泛亮香浓无比的卤汁。面层铺着一块薄薄的油布似的,保护那四十七年的岁月。它天天不断吸收鹅肉精髓,循环再生,天天比昨日更鲜更浓更香,煮了又煮,卤了又卤,熬了又熬,从未更换改变。这是一大桶“心血”。
不过一个人去吃确实有些咸,只能不断的喝水,吃饭,然后慢慢吃不下其他东西。虽然看着不多,最后还是勉强吃完。
东坡肉是我在任何饭店必点的一道菜。其中最让我惊艳的是北京清水亭的黄州东坡肉。清水亭在我心中的地位比湖北当地很多必吃榜还要好吃。而梅苑的东坡肉又是另外一种滋味,五花三层,香而不腻,尤其这过程中还慢慢用小火去煨肉,肉皮的软烂和肥瘦融为一体。也算弥补了在杭州吃东坡肉的遗憾吧。而在北京小厨娘的东坡肉或者红烧肉性价比堪比和平饭店。所以这就是没有感觉到黑珍珠很贵很贵的原因吧。
看着墙上这一堆荣誉,感觉没白来吧。或者说,来都来了,干脆吃好一点。哦对,米饭是免费吃的。真的很友好了。下次要带着朋友一起去吃松鼠桂鱼。
---
## 今天起,不用下载飞书也能用飞书多维表格了!
- URL: https://blog.no-claw.com/posts/c8aadcda/
- 日期: 2025-08-29
- 分类: 硬广
今天起,不用下载飞书也能用飞书多维表格了!
在效率工具圈子里,飞书多维表格一直被公认是“隐藏神器”。
但尴尬的是,它原本只能在飞书 App 里用。
多少人因为要下载飞书、注册账号,被拦在门外:
自由职业者:我就做个客户表,还得多装一个 App?
小团队:光解释注册流程就得半天,协作热情瞬间熄灭。
合作方:我们用企微/钉钉,不想折腾飞书账号。
所以,飞书多维表格虽然口碑好,却始终没能“全民普及”。
但今天,它终于等来了历史性的一刻:
✨ 飞书多维表格支持“单飞”!
👉 不需要下载飞书
👉 不需要注册账号
👉 打开链接就能直接用
入口直达:点击免费体验飞书多维表格
01 为什么这件事值得大书特书?
先别小看这一步。
这意味着:飞书多维表格从一个“附属功能”,变成了任何人都能直接用的独立生产力工具。
更重要的是:它还能和企微、钉钉,甚至企业自研 IM 系统打通,真正做到跨平台无缝协作。
对个人,这是门槛的消失;
对企业,这是数字化转型的加速器。
02 先聊聊“传统表格”的痛点
在它之前,我们用的茶传统表格有以下弊端。
功能强大,但协作差,跨端体验也不够丝滑。
协作好,但在国内体验不算稳定。
轻量好用,但多维度管理和数据库能力有限。
4..颜值高、功能强,但对新手来说学习曲线很陡。
结果就是:要么“太重”,要么“太浅”。
飞书多维表格的出现,正好填补了这个空白:
它既轻量,又专业;既能满足个人使用,又能承载企业核心业务。
03 用下来,我最喜欢的四个爽点
1. 多视图切换 = 一张表多种玩法
表格视图:像 Excel 一样,直观展示数据。
看板视图:任务流转一目了然。
日历视图:做内容排期简直完美。
甘特图视图:项目进度秒变可视化。
画廊视图:适合作品集、商品库。
一句话:一份数据,N 种角度看。
2. 字段关联 = 表格进化成数据库
以前客户和订单分开记,现在只需两张表,一键关联:
点击客户 → 自动列出他的所有订单
点击订单 → 跳转查看客户信息
这不就是轻量版 CRM吗?
3. 自动化 = 表格能自己跑
场景示例:
当任务改成“已完成” → 自动提醒负责人
到期时间临近 → 自动发送通知
新建记录 → 自动打标签
省掉了大量重复劳动。
4. AI 融入 = 不会公式也能飞
飞书多维表格自带 AI:
自动生成公式
一键数据分析
仪表盘智能解读
我曾经花一小时写的统计函数,它直接帮我秒写好。
04 五大核心优势,结合场景更好理解
满血功能:不用下载飞书,也能享受百万行、AI、权限、仪表盘。
→ 中小企业可以低成本搭建业务系统。
AI 融入:会用表格就会用 AI。
→ 比如销售日报,让 AI 自动总结“今天卖得最好的是哪款”。
BI 驾驶舱:拖数据进去就能看。
→ 管理层只要打开仪表盘,就能看清业务趋势。
数据库底座:稳载复杂业务。
→ 核心系统不怕数据爆量,性能跟得上。
零代码搭建:业务人员直接动手。
→ 财务自己搭报销系统,运营自己做会员系统,IT 不再背锅。
05 对个人用户的价值
别以为这是企业专属。对个人和小团队同样好用:
自由职业者:客户管理 + 项目进度,一表搞定。
内容创作者:内容日历 + 数据看板,清晰可见。
学生/社团:活动排期 + 任务分配,协作顺滑。
效率党:习惯打卡 + 知识管理,轻巧高效。
06 为什么要现在就上车?
飞书多维表格的“单飞”,本质上是让生产力工具走向大众化。
以前是“飞书用户的福利”,现在是真正的全民工具。
个人:0 成本体验顶级生产力工具
小团队:低门槛协作,不怕外部伙伴掉链子
企业:跨平台、零代码、可扩展,正好赶上数字化转型潮
总结
今天的飞书多维表格,已经不是一个“辅助表格工具”。
它是一张能动、能算、能协作的表。
不需要下载飞书
不需要注册账号
一张链接,就能开启全新的效率方式
👉 立即免费体验飞书多维表格
👉 官网入口:base.feishu.cn
📌 建议收藏这篇文章:
下次你要做项目排期、客户管理、内容日历、习惯打卡时,直接打开飞书多维表格。
你会发现——表格还能这样玩!
---
## 不建 Hugo、不用 Hexo,纯 Markdown 文件也能接入 Coco-AI!
- URL: https://blog.no-claw.com/posts/df454c34/
- 日期: 2025-08-27
- 分类: 极限科技 / Coco
- 标签: Coco-AI, 极限科技
容器运行 Coco AI,如何访问宿主机的 localhost?
使用容器确实方便了很多事情,但在网络访问上可能会引出一些麻烦。
如果你的调试服务只监听在宿主机的 localhost,那么在容器里访问时,会找的是容器自己的 localhost,所以无法连通。
因为无论是 Coco server 还是 Console 都是服务端发送请求,所以我统一记录下来。
下面介绍几种在不同环境下的解决方案。
1. Mac 的 Orbstack
在 Orbstack 环境中,可以使用 host.docker.internal 代替宿主机的 localhost。
例如访问宿主机的 Hexo 服务(http://localhost:4000/atom.xml)时,直接这样写:
1
http://host.docker.internal:4000/atom.xml
host.docker.internal 会被解析到宿主机 IP,相当于容器内部的 “localhost”。
2. Linux 下单容器运行
在 Linux 环境中,host.docker.internal 默认可能不可用,可以用以下方法:
方法 1:--add-host
运行容器时显式添加:
1
docker run --add-host=host.docker.internal:host-gateway ...
容器里访问:
1
curl http://host.docker.internal:4000/atom.xml
方法 2:--network host
在本地调试时让容器和宿主机共用网络命名空间:
1
docker run --network host ...
这样容器里的 localhost:4000 就等于宿主机的 localhost:4000。
⚠️ 缺点:端口可能冲突,不建议在生产环境使用。
3. Docker Compose 下多容器访问宿主机
在 Linux + docker-compose 场景下,容器访问宿主机的 localhost 同样需要绕过。可以使用以下几种方式(推荐优先使用前两种):
方案 1:host.docker.internal
1
2
3
4
5
6
version: "3.8"
services:
myservice:
image: your-image
extra_hosts:
- "host.docker.internal:host-gateway"
容器里访问:
1
curl http://host.docker.internal:4000/atom.xml
方案 2:Docker 网桥网关 IP
Linux 默认 docker0 网桥的宿主机 IP 通常是 172.17.0.1,可用以下命令确认:
1
ip addr show docker0
容器里直接访问:
1
curl http://172.17.0.1:4000/atom.xml
⚠️ 缺点:如果 Docker 网络结构改动,IP 可能变化。
方案 3:network_mode: host
1
2
3
4
services:
myservice:
image: your-image
network_mode: host
容器内的 localhost:4000 直接访问宿主机服务。
⚠️ 缺点同上,失去网络隔离,端口冲突风险高。
方案 4:绑定 Hexo 到 0.0.0.0 并用局域网 IP
容器里访问:
1
curl http://192.168.x.x:4000/atom.xml
其中 192.168.x.x 为宿主机的局域网 IP。
💡 建议:如果 Compose 版本 ≥ 3.4,优先使用 方案 1。写死 host.docker.internal 后,即使宿主机 IP 变化,也能稳定访问。
通过上面的几种方式,无论是在 Orbstack、Linux 单容器 还是 Docker Compose 场景下,都能找到合适的方法让容器访问宿主机的 localhost 服务。
日常调试时,推荐优先使用 host.docker.internal(配合 --add-host 或 Compose 的 extra_hosts),既稳定又无需记 IP;
在容器之间互访,则直接使用 服务名/容器名,让 Docker 自带的 DNS 帮你解析。
掌握这些技巧,既能让 Coco AI 的调试环境跑得顺畅,也能为后续复杂的容器网络架构打好基础。
---
## 毕业很久了,女同学突然找来修电脑.......
- URL: https://blog.no-claw.com/posts/4b7a2922/
- 日期: 2025-08-27
- 分类: 电子产品 / 电脑
- 标签: MacOS
之前合作的过的文科姑娘,来问我电脑问题,是一个关于文件无法删除的问题。报错无法完成此操作,因为发生意外错误(错误代码-8062)。
我起初以为很简单的事。
WPS 没关? —> 关闭软件或者重启 (没用)
文件锁定 —> 解锁或者 chmod (rwx
用 CLI 直接删除。 —> rm xxx.pdf (提示 time out????)
微信发给我看看 –> 能正常删除
1
ls -l nfs server : not responding total 3580 -rwxrwxrwx@ 1 root wheel 1832497 4 23 2009 e.pdf
最后在这里找到了答案,由于之前她使用 icloud 同步了整个磁盘,icloud 很快就会满了。再次扩容之后虽然还是保持了卡死的状态。毕竟 apple 这德行。我还经历过把我的 icloud 日程自动同步到其他人的 icloud 里的操作。按照 apple 售后支持这个德行,跟他们说了也是白说。还是直接搜论坛吧:
https://discussions.apple.com/thread/253122585?sortBy=rank
虽然用 ctrl+command + delete 也能删除,但是感觉是元数据层面?
---
## 硬件探索记 Tuya 篇(一):初识 T5 AI 开发板,先配置个环境吧
- URL: https://blog.no-claw.com/posts/ab7870f7/
- 日期: 2025-08-27
- 分类: 电子产品 / 纯硬件
- 标签: 单片机, Tuya
之前因为 AI 小智才玩过 ESP32S3,摸索了一遍 ESP-IDF 的开发流程。最近又被朋友种草了 Tuya,正好申请到了一块开发板,就顺便来玩玩。板子第二天就寄到了,还额外附送了一块屏幕。因为出门只带了 联想口红电源,所以我直接用它给开发板供电(当然,直连电脑也可以完成烧录和调试)。
玩硬件,其实也是在找回一些失去的记忆。
TuyaOpen SDK 简介
这次用到的 SDK 叫 TuyaOpen,它的定位类似 ESP-IDF CLI。
TuyaOpen 同时也支持开发者跨芯片平台、操作系统开发智能设备,代码开源。
跨芯片平台是指支持一些系列芯片,包括 T3, T5 AI,ESP32 等等开发板。
跨操作系统是支持 MacOS,Windows,Linux 全平台开发。对 Mac 党超级友好。
一样是从 GitHub 拉取开发工具链
支持编译、烧录、串口调试
提供了官方示例(apps/tuya_cloud 下有很多 demo)
这套工具链对习惯了 ESP-IDF 的人来说几乎是“无缝迁移”。
开发环境配置(macOS 示例)
我这边使用的是 macOS 环境,常用命令如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 1. 克隆源码
git clone https://github.com/tuya/TuyaOpen.git
# 2. 加载环境变量(类似 ESP-IDF 的 export.sh)
. ./export.sh
# 3. 进入示例工程
cd apps/tuya_cloud/switch_demo
# 4. 配置工程(类似 idf.py set-target xxx)
tos.py config choice
# 5. 编译工程
tos.py build
# 6. 烧录到开发板
tos.py flash
# 7. 打开串口监控
tos.py monitor
注释说明
tos.py config choice:配置芯片型号或项目参数,和 idf.py set-target 类似
tos.py build:编译整个工程,输出 bin 文件
tos.py flash:将编译好的程序写入开发板
tos.py monitor:打开串口调试,实时查看日志
这个过程第一次会拉一堆开发工具链。
✅ 小贴士:tos.py 本质上和 idf.py 是同一个思路,习惯了 IDF 的命令行,就能很快上手。
不过需要注意的是,这三个命令并不能连在一起使用,这个是和 ESP IDF 不同的地方。
在 Linux 下大家习惯用 lsusb,而在 macOS 上则需要借助系统自带命令。
方式一:system_profiler
1
system_profiler SPUSBDataType
能看到所有 USB 总线和设备的详细信息。
方式二:ioreg
1
ioreg -p IOUSB
会以树状结构列出 USB 设备。
方式三:安装 usbutils(提供 lsusb 命令)
我自己装了 usbutils,所以也能直接跑:
1
2
3
4
➜ ~ watch -n 1 lsusb
➜ ~ lsusb
Bus 000 Device 001: ID 1a86:55d2 1a86 USB Dual_Serial Serial: 5AAE169301
Bus 000 Device 000: ID 1a86:55d2 1a86 USB 3.1 Bus
✅ 小贴士:串口设备在 macOS 下一般表现为 /dev/cu.usbserial-xxxx 或 /dev/cu.usbmodemxxxx,后面烧录和 monitor 要用到。
编译与运行
在 M2 MacbookPro 上编译速度还不错,只是偶尔 CPU 会满载:
等终端出现 编译成功 (Build done) 的提示,就说明一切正常,可以进入下一步。
串口通信
最后就是进入串口调试:
此时如果开发板正常启动,就能在日志里看到打印输出。后续无论是控制外设还是调试联网,串口监控都是必不可少的。
谁说只有 windows 才能玩单片机的。。。
---
## 硬件黑客松的感受
- URL: https://blog.no-claw.com/posts/9a6d478a/
- 日期: 2025-08-27
- 分类: 电子产品 / 纯硬件
- 标签: 单片机
参加了 Rebuild-z 的硬件黑客松,马上结束了觉得基本没怎么睡好。本来想来旅游玩玩散心的。结果也不比上班轻松。写点感受和教训。
先说教训
使用了自己不熟悉的技术栈,比如之前用过 stm32,感觉 esp32 在物联网智能上比较成熟。结果下载固件就浪费了一天(缺少 CH343 驱动)
临时组队,一直在讨论做什么,怎么做,以及在软件上花费的时间过多。
倒反天罡,使用 MacOS 搞硬件,抛开生态不谈,应该是全场唯一一个,遇到问题不好找人。
步子迈得太大了。打算从零画 PCB,然后部署 LLM 端到端的流程。结果哪哪都是坑。。。
组队成员背景差异巨大,很多事情没对齐。任务进度跟踪不好。
再说说感受
很多 00 后,技术不错,也看不出来是 00 后。年轻人很有热情,打工人眼里没有光了。
很久不正八经写代码了。手生了。
见到了很好玩的东西,能够解答之前的疑惑。算是没白来,比如和队友跑了 AI 小智,也在隔壁组找到了电视盒子的平替方案。
创业的氛围和进厂螺丝钉打工真的很不一样。(除了把产品卖出去)
爱好是爱好,把事情做成还得好好磨练下。
---
## 高铁上错车以及误车怎么办?
- URL: https://blog.no-claw.com/posts/9ff7e5aa/
- 日期: 2025-08-25
- 分类: 电子产品
苏州到高铁的车很多,也很密。才过安检就看到车在等着,03 车 14 看成了 14 车,可能熬夜后遗症,不过也不是第一次这么干了。然后打算从中间穿过去,因为两节车厢拼接,这边只能到,能到 9 车。于是在无锡下车换乘,再一路狂奔到指定位置。询问列车员后,才知道这车比原来的车快了 5 分钟,前车走,后车止,于是眼花缭乱,上错花轿。算是一个经典案例了。
但是吧,也不是什么严重问题,经常有人搞错,所以正常出站就好了。万一真的错过呢?还可以改签,如果改签过了,找铁路工作人员呗。。。。。。
PS: 吐槽苏州火车站不能刷 12306 二维码进站,然后和复读机吵起来了。我能有什么坏心思呢,单纯的包太多,身份证不好找而已。
---
## 如何白嫖嘉立创每个月免费打板?
- URL: https://blog.no-claw.com/posts/de1fd60a/
- 日期: 2025-08-11
- 分类: 电子产品 / 纯硬件
- 标签: 硬件
首先,搜索嘉立创下单助手,需要用这个 APP 来领取优惠券。
左下角 - 优惠群中心 - 免费 PCB 券。每个月可以领取两张。
然后领取这个 1-4 层专用券,可以抵扣 20 元。不完全测试下仅限标准套餐,打磨什么的需要额外加钱,不能抵扣。
然后回到 PCB,有两种办法
直接在软件页面下单
导出 gerber 文件下单
首先确定 DRC 通过,没有问题。
或者导出还是嘉立创主页:https://www.jlc.com/
然后就会跳转到下单的平台,我们可以选择 PCB 的类别和参数,数量就选择 5 个,右边显示价格的地方可以选择优惠券。
选择了优惠券的后,价格直接变成免费。
出货方式选择单片就行,然后板厚 1.0 - 1.6。颜色除了黄色这种冷门工艺,基本都是在免费范围。(可能会有调整)
绿色的板子是最快的,其他颜色会慢 1-2 天。
个性化服务大多数都需要额外付费,所以保持默认就好。
还有一个加 PCB 添加标识的。按照需要使用就好。
SMT 和刚网通通不需要。然后选择快递下单就可以了。
剩下就是收板子。
---
## 在 Coco AI 中接入 WordPress RSS,实现文章秒级搜索
- URL: https://blog.no-claw.com/posts/2bdb8c4a/
- 日期: 2025-08-10
- 分类: 极限科技 / Coco
- 标签: Coco-AI, 极限科技
随着内容创作者不断积累文章,如何让自己的内容被快速检索、精准找到,成为提升网站体验的重要一环。
尤其是对于使用 WordPress 搭建博客或官网的朋友来说,文章虽多,但用户往往需要翻页或依赖站内搜索才能找到所需内容。而如果能把 WordPress 的文章源接入 Coco AI,不仅能实现秒级检索,还可以结合 AI 进行智能问答、聚合分析,让你的内容价值成倍提升。
今天我就来分享一下,如何用 WordPress 自带的 RSS 功能,把文章无缝接入到 Coco AI 中,实现一键搜索全站文章。
1. 准备工作
在开始之前,你需要:
已经安装好的 WordPress 网站(如果不会安装,可以参考我之前的文章)
👉 WordPress 安装教程
有一个可用的 Coco AI 服务端(本地部署或云端均可)
2. 获取 WordPress RSS 链接
WordPress 默认的 RSS 地址格式非常简单:
1
http(s)://你的域名/feed
为了确保 RSS 链接正常工作,你可以使用 Fluent Reader 这样的本地 RSS 阅读器测试一下。
如果能正常读取文章列表,说明 RSS 输出正常:
3. 设置 RSS 输出数量与摘要长度
为了让 Coco AI 一次获取更多的文章,你可以在 WordPress 后台调整 RSS 输出数量:
文章数量:一次输出多少篇文章(根据需求设置,建议 20~50 篇)
摘要长度:决定 AI 抓取的内容字数,摘要越长,AI 的理解能力越强
4. 在 Coco AI 中添加 RSS 连接器
接下来进入 Coco AI 管理后台,新建一个 RSS 连接器:
在“数据源”中选择 RSS
填写刚刚获取的 WordPress RSS 链接
设置刷新频率(例如每 5 分钟、15 分钟)
配置完成后,你会在连接器列表中看到刚添加的 WordPress RSS 源:
5. 刷新并查看文章
等待 Coco AI 进行第一次数据抓取,稍后就能在文章列表中看到你的 WordPress 文章:
在 Coco AI 客户端的搜索栏中,直接输入关键词,就能实时搜索 WordPress 全站文章:
点击搜索结果,Coco AI 会直接跳转到对应的 WordPress 文章主页:
6. 使用场景与优势
通过这种方式接入 RSS,你可以:
快速检索:秒级找到 WordPress 上的历史文章
跨平台聚合:和其他数据源一起接入 Coco AI,实现跨网站统一搜索
AI 智能问答:基于你的网站内容,直接生成读者提问的答案
知识库构建:将博客内容沉淀为长期可用的知识库
只需几个步骤,就能让 WordPress 与 Coco AI 打通,实现全站文章的智能化搜索与访问。
如果你的网站内容很多、更新频繁,这种方法能极大提升读者和你的检索效率,让你的文章真正“活”起来。
---
## Coco AI 服务端文件系统检索
- URL: https://blog.no-claw.com/posts/ca3451f/
- 日期: 2025-08-10
- 分类: 极限科技 / Coco
- 标签: 极限科技, Coco AI
随着企业和个人数据量的激增,如何高效管理与搜索文件资料,已成为提升工作效率的关键。
Coco AI 新增的 本地文件连接器,可以直接接入服务端文件系统,实现秒级搜索、即时访问,让服务器上的文件像本地文档一样触手可及。
本文将介绍如何通过 Docker 快速部署 Coco Server,并配置本地文件连接器,实现服务端文件的智能检索。
一、快速部署 Coco Server
Coco Server 是连接器功能的核心组件,部署完成后即可接入本地文件、RSS 等多种数据源。
生产环境建议使用持久化存储,避免数据丢失。
1. 推荐部署方式(生产环境)
1
2
3
4
5
6
7
docker run -d \
--name cocoserver \
-p 9000:9000 \
-v data:/app/easysearch/data \
-v config:/app/easysearch/config \
-v logs:/app/easysearch/logs \
infinilabs/coco:0.7.1-2426
2. 测试部署方式(非持久化)
1
2
3
4
docker run -d \
--name cocoserver \
-p 9000:9000 \
infinilabs/coco:0.7.1-2426
建议生产环境使用持久化部署(第一种方式),测试环境可选择非持久化部署(第二种方式)。
二、配置 AI 模型
创建用户后,我选择 Ollama 作为模型提供商:
地址:http://localhost:11434
模型:deepseek-r1:7b
在「模型提供商」界面可以看到默认启用的 Coco AI,它会直接调用已配置的 Ollama,也支持任何兼容 OpenAI API 协议的 LLM。
三、数据源概览
Coco AI 默认内置了 官方文档 和 Hacker News 数据源,并在近期新增:
本地文件连接器(本文重点)
RSS 连接器
S3 连接器
四、配置本地文件连接器
本地连接器的配置非常简单,只需:
选择文件路径
设置需要索引的文件后缀
等待系统从 localFS 中智能提取内容
使用 Docker 时的注意事项
如果通过 Docker 部署 Coco Server,需要将本地目录映射到容器内,因为连接器读取的是容器内部路径,而非主机路径。
当然,也可以像我这样直接在 Orbstack 等容器平台上传文件,省去目录映射的步骤。
添加完成后,可以在连接器列表中看到新建的服务端本地文件连接器:
五、在 Coco App 中查看与检索
登录 Coco App 后,可以看到刚刚添加的 本地文件 数据源,并直接进行搜索。
这是刚才添加的服务端文件的搜索结果:
此外,Coco AI 还支持客户端本地文件搜索,但本文重点展示的是服务端文件检索功能:
结尾
通过本地文件连接器,Coco AI 不仅能帮助你把服务端的文档、日志、配置文件快速接入 AI 检索,还能结合多数据源进行统一搜索,大幅减少人工查找和信息碎片化的时间成本。
未来你还可以将 本地文件检索 与 RSS、API、数据库连接器组合使用,让企业级信息管理更智能、更实时、更高效。
如果你也想让服务器里的海量资料触手可及,不妨部署一个试试——也许你的检索方式,从今天就会不一样。
---
## 手把手教你使用 Coco AI 订阅RSS,智能检索Hexo博客
- URL: https://blog.no-claw.com/posts/47752b69/
- 日期: 2025-08-10
- 分类: 极限科技 / Coco
- 标签: 极限科技, Coco AI
最近 Coco AI 上线了几个新功能:S3 连接器、本地文件连接器、RSS 连接器。本篇先重点讲 RSS 连接器检索 HEXO 博客的接入方法。
一、安装 Coco Server
使用 Docker 部署是最省心的方式。
方式 1:映射数据目录(推荐)
1
2
3
4
5
6
7
docker run -d \
--name cocoserver \
-p 9000:9000 \
-v data:/app/easysearch/data \
-v config:/app/easysearch/config \
-v logs:/app/easysearch/logs \
infinilabs/coco:0.7.1-2426
方式 2:快速测试(不映射目录)
1
2
3
4
docker run -d \
--name cocoserver \
-p 9000:9000 \
infinilabs/coco:0.7.1-2426
测试环境可以用方式 2,生产环境建议使用方式 1,避免数据丢失。
二、模型配置
创建完用户后,我直接设置了 Ollama 作为模型提供商:
地址:http://localhost:11434
模型:deepseek-r1:7b
在「模型提供商」界面可以看到默认开启的 Coco AI,它会直接调用我配置的 Ollama,也支持其他兼容 OpenAI API 的 LLM。
三、数据源概览
Coco AI 默认植入了官方文档和 Hacker News 数据源,这次新增了三类连接器:
S3 连接器
本地文件连接器
RSS 连接器(本篇重点)
四、Hexo 拓展 RSS 功能
我们先来安装 Hexo:
1
2
3
4
pnpm install -g hexo-cli
hexo init my-blog
cd my-blog
pnpm install
Hexo 默认没有开启 RSS,需要通过插件来支持。你可以这样做:
1. 安装 hexo-generator-feed
1
pnpm add hexo-generator-feed
2. 配置 _config.yml
在 Hexo 根目录的 _config.yml 里加上:
1
2
3
4
feed:
type: atom # 可选: atom / rss2 / json
path: atom.xml # 输出文件路径
limit: 20 # 0 表示不限制数量
3. 生成
1
pnpm hexo clean && pnpm hexo generate
生成的 RSS 会在 public/atom.xml
五、添加 RSS 连接器
选择 RSS 连接器,比如本地调试模式是 localhost:4000:
输入 RSS 地址(这里我用的是我的博客):
1
http://host.docker.internal:4000/atom.xml
刷新时间设为 1 分钟(默认即可)
添加完成后可以看到我同时接入了 S3、本地文件和 RSS:
六、在 Coco App 中查看数据源
登录 Coco App 后,可以看到刚才添加的 S3、本地文件和 RSS 数据源:
七、搜索效果
使用 Coco-AI 搜索时,能快速检索到 RSS 中的内容,效果比博客自带的好很多:
总结
通过 RSS 连接器,Coco-AI 可以实时抓取和索引博客内容,并与本地文件、S3 数据等统一搜索,非常适合做多源聚合知识库。
如果 RSS 输出有限,可以调整博客端的 RSS 配置,让它输出更多历史内容,发挥 Coco AI 检索的最大价值。
---
## 学云计算到底是在学什么?
- URL: https://blog.no-claw.com/posts/4b1e0fb/
- 日期: 2025-08-10
- 分类: 软件 / AWS
- 标签: AWS
个人感觉 AWS 甚至很多云计算平台都被过分神话了。以下是个人经验,不喜勿喷。
人话版本
学什么?
怎么启动虚拟机,以及如何进行远程管理,比如 SSH 和 DRP。
对象存储 S3。如果你能自建 MinIO 或者 RustFS 也没问题,如果你喜欢 OpenStack Swift 也行。
Docker/K8S。这帮人天天吹云原生,其实主要还是容器充分可以利用云上的弹性。
网络基础,比如为什么我连不上某台机器,能从网络链路都排查一遍。
为什么还有各种组件?
举几个例子:
中间件或者数据库软件很多都有 PaaS 版本,主打一个“开箱即用”。好处是一键配置多可用区,一键安装软件,让开发人员不用考虑多余的事。
曾经的槽点:某些云厂商直接把开源软件托管来卖,所以才有后来一些厂家改商用 License 的事。
分布式计算软件安装繁琐,对小白友好直接控制台点点点,文科生也能学会。
都上云了,你可以不用,但云厂商一定要有,否则对不起“宇宙厂”的名头。
上了云成本就一定小吗?
不一定,需要在可用性、经济性、性能之间取舍。
很多 HA 的功能是冗余的,分布式软件自己带了一套,云上又做了一套 Standby。
除非买年度订阅有很大折扣,否则成本不可控。
都在云上,网络延迟肯定比线下 DC 要大。
如果你是纯 C 端用户,玩玩得了,你的这点用量人家根本看不上。
上云的好处在哪里?
钱花到哪里更加明确了。
PPT 能做得更好看。
产品可以快速出 MVP,能够对全套链有个完整认知。
有些服务可以 Pay as you go。
公网 IP 自由,不过也开始陆续收费了。
减少运维成本,出问题压力直接甩给厂商。
我是一个小白,买软件送服务吗?
国内云有些支持免费,但海外云需要付费购买售后支持,可以帮你找文档、讲解服务用法、错误排查等。
Serverless 普遍做得很烂,你学习平台规则的成本大于你在平台上部署的成本。
想啥呢?文档都那么烂。
国内和国外云生态对比
海外很多 PaaS 或 SaaS 都基于 AWS 来做,所以看起来国外云计算很火。
国内软件开发和云计算很割裂,整体还是以 SDE 为导向,跟云厂商打交道最多的还是运维。
一些遵守当地合规的云公司夹在中间更难受,选择需谨慎。
总结
如果个人爱好者真的有需要,还是自己搭建 Homelab 吧。控制好噪音和功耗就行。
---
## 大上墨水屏测评
- URL: https://blog.no-claw.com/posts/2fca5ee8/
- 日期: 2025-08-09
- 分类: 电子产品 / 外设
- 标签: 外设, 显示器
在这个几乎人人都离不开电子屏幕的时代,墨水屏显示器正在悄悄进入更多人的视野。相比传统的 LCD 或 OLED,墨水屏的护眼、低功耗和接近纸张的阅读体验,让不少文字工作者和重度阅读用户心动。
这次入手的大上 Paperlike 系列墨水屏,使用下来有了不少感受。本文就从外观、使用体验到一些细节槽点,和大家聊聊我的真实体验。
最早接触墨水屏显示器,是前年在北京的 SKP。很多年前使用过 kindle,那时候去看墨水屏平板。正巧看到了墨水屏显示器,在店里试用了下很喜欢。相对于很古早的 kindle 而言,国产化的产品更加符合国人的使用习惯化。
和其他产品不同的是,大上的墨水屏有彩色版,所以可以当作外接显示器来用。这么多天使用下来,我比较喜欢的产品的地方竟然是看视频,比如说看《游戏王》,感觉就像是漫画一样。
我的机器是 Paperlike253 的 Mac 版本,到手之后先拍了外观一览,总体很好看,奈何桌子空间内有限,实在摆不下了。M2 的 Macbook pro 只能同时连接两个设备,所以基本是一个外接 4K 屏幕和一个外接墨水屏使用的状态。
相对于最早的 Kindle 来说,大上墨水屏已经有质的飞跃,大上的革命者系列竟然能达到 33HZ 的“高刷”,甚至可以播放一部分的视频。毕竟很早以前的 kindle,连翻页都感觉卡。我这些日子挺喜欢用墨水屏来看游戏王的,虽然是日漫,但是开了视频模式,感觉像是漫画一样。
无论什么电子产品,只要跟 Mac 相关的都需要先做特殊的配置,Mac 版本的墨水屏需要安装客户端来稳定画质,因为苹果的限制。革命者系列自带了一个 HDMI,TYEPC 的线,还有一根 USB-A 转 USB-B 的线。USB 的线是用和驱动软件通讯的。
这里有两个槽点:
Typec 只用用来传输数据,并不能给 Macbook 供电。
Macbook 或者 Macmini 都没用 USB-A 的接口了,所以这个线还要转接一下。
这里有一个小技巧就是如果你是双屏幕的情况下,可以把另外一个显示器当作 HUB 用。这又何尝不是一种菊花链?不过还是觉得能够一线通就更好了~
墨水屏显示器的电源适配器是 12V3A,也就是 36W。这个功率在显示屏的圈子里已经很低了。个人感觉后面也可以出一些直流供电的版本,然后就可以使用移动电源或者笔记本 C2C 进行供电。经验而谈,墨水屏在不刷新的时候基本是不耗电的,个人推测这个 36W 主要还是给背光灯使用的。
言归正传,介绍外观。25.3 寸的屏幕使用起来没有比我的 28 寸显示器体感少太多。机身十分简洁,只有左下角有六个按键,分别对应:
C:短按手动刷新,长按记忆模式。
M:切换模式,有自动模式,文本模式,图文模式以及视频模式。
+/- : 对比度调节大小,右边是色温调节,比如
大大的按键,按压起来软软的,给人一种重剑无锋的感觉。使用起来十分方便。没有花里胡哨的功能菜单,更像是大人的玩具。
显示器背面是标准的 10 * 10 的 VESA 接口,所以这个显示器支架也可以换成市面上通用的支架壁。自带的显示器支架也自带旋转和伸缩的功能,相对于很多显示器自带的支架已经好了很多了。
说回屏幕本身,在熄屏状态,以及没有连接数据线或者没有开启客户端的时候,墨水屏会显示需要信号的图案。这个在致敬有线电视时代的无信号,很有意思。
屏幕有背光灯,所以晚上使用的时候不用担心光源的问题。如果不连接电脑的话,晚上就不会触发这个。
墨水屏同样也支持 HIDPI,在我的 M2 Pro Macbook Pro 上最高支持 1600 * 900 的 HIDPI。基本上就是把原来的 3K 放大了一倍,这一点比很多 3.5K 带鱼屏的 HIDIPI 好很多。之前公司的带鱼屏外接 Mac 总会把下半部分截断,使用起来体验很差,但是在大上的屏幕上就没有这个问题,毫无疑问,这是一个真正合格的 Macbook 外接屏幕。
因为这个事,我还特意调查了分辨率以及 PPI 对 MacOS 外接的影响。大上墨水屏外接几乎没啥违和的地方,屏幕分辨虽然不是 4K,但是在开了原生 HIDPI 使用下完全没有问题。问了客服 PPI 是 145,虽然还达不到 MacOS 的 200+,已经可以达到外接使用的程度了。
计算屏幕 PPI(像素密度)的公式是:
1
2
3
\[
PPI = \frac{\sqrt{(\text{宽像素})^2 + (\text{高像素})^2}}{\text{屏幕对角线尺寸(英寸)}}
\]
对比参考:32 寸 4K 屏幕 ≈ 138 PPI27 寸 4K:≈ 163 PPI
然后再说说这几种模式,图文模式主要针对混排的场景,这样图片能够显示清晰一些。最清晰的还是文本模式,适合纯文本,看电子书,文字网页,这个效果堪比 LCD 显示器。看视频一定要用视频模式(有点废话)。自动模式我很少用。
在几种模式里,我最喜欢的是文本模式,其次是视频模式。文本模式用来看电子书,就是最古早的 Kindle 那种看电子书的感觉,白纸黑字的感觉最好了。这个模式也能用来刷网课,比如一些理论公式什么的,效果竟然比视频模式要好的多。
视频模式主要用来刷剧,比如前面提到的看游戏王。感觉就是在看漫画。另外我在设计 PCB 电路板的时候,使用墨水屏来看渲染模型,也有种物理书的即视感。
美中不足的事看 MV 的效果不算是很好,尤其有些 MV 会出现颜色过暗的问题。还是看日漫吧 hhhh
在不同模式之间切换需要调节合适的亮度和对比度。比如黑色的场景就把对比度调亮一些。文本模式就可以暗一些,这样文字回更加清晰。
刷新率能够达到宣传的 33HZ,尽管和主流 LCD 显示器相比在窗口拖拽方面还是有较大的差距,但是在文字阅读和日常上网方面已经几乎可以当作主力设备来使用了,比如买一个 Macmini,然后外接这样一个显示器无论老人和小孩都能用。(前提是得解决 Paperlike 客户端开机自启动的问题)
在拓展屏的模式下 ufotest 有这个报错,在把墨水屏设置为主显示器之后可以正常显示了。
1
2
ufotest SYNC FAILURE:
Move all apps and browser windows to primary monitor #1.
背景尽量纯色,这样可以让显示效果更好一些。再来说残影,这是一个不可避免的问题,所以可以在客户端中设置刷新时间,也可以左下角使用 C 按键手动刷新。
墨水屏用着用着就会困,应该是没有蓝光辐射的原因,作为一个科技从业人员,每天面对电子产品和蓝光的辐射,对健康是一个不小的伤害,很多时都在默默承受这一切,虽然慢慢下意识习惯了这一切,但是身体从未忘记。
墨水屏写作,多了一份静心。让我们回归最早的阅读,把电子化和纸质的感觉合二为一可能是未来的趋势。起码现在对于文字工作者来说,在文本模式下写作已经可以很专注了。
总的来说,大上的墨水屏在同类产品里已经算是头部代表,无论是作为护眼阅读设备,还是作为 Mac 的外接显示器,都有着不俗的表现。虽然它现在并不能完全取代传统高刷显示器,但在文字工作、漫画视频和长时间阅读方面,确实提供了一种更加健康和沉浸的体验。
在“屏幕无处不在”的今天,墨水屏或许正在开辟另一条道路。对我来说,它既是工具,也是让人慢下来的媒介。希望未来大上的墨水屏能越做越好,真正成为数字生活与纸质体验之间的桥梁。
---
## Coco AI × Amazon S3:秒搜你的云端文件
- URL: https://blog.no-claw.com/posts/f050840/
- 日期: 2025-08-09
- 分类: 极限科技 / Coco
- 标签: 极限科技, Coco AI
随着企业和个人数据量的激增,如何高效管理与搜索云端资料,成为提升工作效率的关键。
Coco-AI 新增的 S3 对象存储连接器,可以将 Amazon S3 存储桶直接接入智能检索系统,实现秒级搜索、即时访问,让云端文件像本地文档一样触手可及。
本篇将详细介绍如何通过 Docker 快速部署 Coco Server,并配置 S3 连接器,完成与亚马逊云科技的无缝集成。
一、快速部署 Coco Server
Coco Server 是连接器功能的运行核心,部署好它后才能接入 S3。
生产环境建议使用持久化存储方式,避免数据丢失。
推荐部署方式(生产环境)
持久化存储,避免数据丢失:
1
2
3
4
5
6
7
docker run -d \
--name cocoserver \
-p 9000:9000 \
-v data:/app/easysearch/data \
-v config:/app/easysearch/config \
-v logs:/app/easysearch/logs \
infinilabs/coco:0.7.1-2426
测试部署方式(非持久化)
1
2
3
4
docker run -d \
--name cocoserver \
-p 9000:9000 \
infinilabs/coco:0.7.1-2426
建议生产环境使用第一种部署方式,测试环境可选择第二种。
二、配置 AI 模型
创建用户后,我选择 Ollama 作为模型提供商:
地址:http://localhost:11434
模型:deepseek-r1:7b
在「模型提供商」界面可以看到默认开启的 Coco AI,它会直接调用我配置的 Ollama,也支持其他兼容 OpenAI API 的 LLM。
三、数据源概览
Coco AI 默认内置官方文档和 Hacker News 数据源,近期新增三类连接器:
S3 连接器(本篇重点)
本地文件连接器
RSS 连接器
四、接入 Amazon S3
选择 S3 对象存储连接器
填写 Endpoint(例:东京区 s3.ap-northeast-1.amazonaws.com)、Bucket 名称、亚马逊云科技凭证(Access Key ID / Secret Access Key),刷新间隔建议保持 1 分钟 默认值。
获取亚马逊云科技访问凭证
登录 亚马逊云科技 IAM 控制台
创建访问密钥(Access Key ID / Secret Access Key)
为用户分配最小化 S3 访问权限(推荐遵循最小权限原则)
这里选择访问密钥 - 创建访问密钥,然后保存 Access Key ID / Secret Access Key 就好。
创建过程中会出现最佳实践提示,不影响后续配置,下载密钥即可使用。
其他的凭证方式虽然有 IAM Role 和 Role anywhere,但是我们这次不会用到。
确保这个用户有访问 S3 的权限,如果是生产的环境的话,确保要采用最小权限原则来防止不必要的麻烦。如果你在存储桶上配置了对应桶策略也可以。
对象前缀(Prefix)配置
这个是我在 S3 上的对象存储,放了一些 markdown 文件上去。
还是这张图,我使用的是东京区的存储桶 dify233,所以 endpoint 是 s3.ap-northeast-1.amazonaws.com。
这里的对象前缀可以理解为目录,在 S3 设置之初会把所有文件夹的名称当作前缀加到文件名前面,所以也有 S3 是扁平化管理一说。
五、集成效果
完成连接后,S3 中的 Markdown 文件可被 Coco AI 实时索引与检索,点击搜索结果即可跳转到 S3 公网访问链接,例如:
1
https://.s3..amazonaws.com/<对象名>
不仅支持标题关键词搜索,还可结合 LLM 实现语义检索,极大提升信息获取效率。
添加完成后可以看到我同时接入了 S3、本地文件和 RSS,我们这里主要开介绍关于 S3 的连接器。
使用 Coco-AI 搜索时,能快速检索到 s3 中的 markdown 文件。
点击搜索结果可直接跳转到对应链接。
也支持把地址复制出来:https://dify233.s3.ap-northeast-1.amazonaws.com/对象名,其实就是S3的https 链接了。
六、适用场景
企业内部知识库:研发文档、政策文件、培训资料统一存放于 S3
个人云端资料管理:博客、项目资料随时调用
跨团队协作:多地访问,实时共享
通过 Coco AI S3 连接器,只需几步,即可让 ** Amazon S3** 成为高效智能检索系统的云端引擎。
无论是个人开发者,还是大型企业团队,都能快速构建跨云端、本地、第三方数据源的统一知识平台。
---
## 从零到用:RSS 接入 Coco-AI 实战指南
- URL: https://blog.no-claw.com/posts/2c28c43/
- 日期: 2025-08-09
- 分类: 极限科技 / Coco
- 标签: Coco-AI, 极限科技
最近 Coco-AI 上线了几个新功能:S3 连接器、本地文件连接器、RSS 连接器。我会逐一介绍,本篇先重点讲 RSS 连接器的接入方法。
一、安装 Coco Server
使用 Docker 部署是最省心的方式。
方式 1:映射数据目录(推荐)
1
2
3
4
5
6
7
docker run -d \
--name cocoserver \
-p 9000:9000 \
-v data:/app/easysearch/data \
-v config:/app/easysearch/config \
-v logs:/app/easysearch/logs \
infinilabs/coco:0.7.1-2426
方式 2:快速测试(不映射目录)
1
2
3
4
docker run -d \
--name cocoserver \
-p 9000:9000 \
infinilabs/coco:0.7.1-2426
测试环境可以用方式 2,生产环境建议使用方式 1,避免数据丢失。
二、模型配置
创建完用户后,我直接设置了 Ollama 作为模型提供商:
地址:http://localhost:11434
模型:deepseek-r1:7b
在「模型提供商」界面可以看到默认开启的 Coco AI,它会直接调用我配置的 Ollama,也支持其他兼容 OpenAI API 的 LLM。
三、数据源概览
Coco-AI 默认植入了官方文档和 Hacker News 数据源,这次新增了三类连接器:
S3 连接器
本地文件连接器
RSS 连接器(本篇重点)
四、添加 RSS 连接器
选择 RSS 连接器
输入 RSS 地址(这里我用的是我的博客):
1
https://airag.click/atom.xml
刷新时间设为 1 分钟(默认即可)
添加完成后可以看到我同时接入了 S3、本地文件和 RSS:
五、小插曲:RSS 条目数量限制
一开始我发现 Coco-AI 只能显示 最近 20 条,以为是 Coco Server 的限制,后来群友提醒才发现是 RSS 服务端设置的问题。
在博客服务端调整配置后,RSS 就能显示 全部文章 了。虽然很多 RSS 只显示最近内容,但其实可以通过配置让它输出完整数据。
六、在 Coco App 中查看数据源
登录 Coco App 后,可以看到刚才添加的 S3、本地文件和 RSS 数据源:
七、搜索效果
使用 Coco-AI 搜索时,能快速检索到 RSS 中的内容,效果比博客自带的好很多:
点击搜索结果可直接跳转到博客文章。我用的是 Hexo 主题,其他 RSS 源也一样适用。
总结
通过 RSS 连接器,Coco-AI 可以实时抓取和索引博客内容,并与本地文件、S3 数据等统一搜索,非常适合做多源聚合知识库。
如果 RSS 输出有限,可以调整博客端的 RSS 配置,让它输出更多历史内容,发挥 Coco-AI 检索的最大价值。
---
## Google SEO 技巧
- URL: https://blog.no-claw.com/posts/3d393e74/
- 日期: 2025-08-08
- 分类: 软件
- 标签: SEO
https://www.google.com/ncr
这个入口就是让它不要应用特定国家重定向(No Country Redirect)
---
## 普通人需要哪些电脑知识?
- URL: https://blog.no-claw.com/posts/87d5ac58/
- 日期: 2025-08-08
- 分类: 电子产品 / 电脑
- 标签: 外设
作为一个 N 多年的电脑社畜,日常使用 window/Linux/MacOS 三系统,写点最近被问到的问题。
请问您觉得有什么我们普通人可以尝试的一些电脑软件方面的操作呢,这些前沿的知识蛮重要,一般人很难知道 🌹
装机,如果追求不多,弄个装系统的 U 盘就行,Windows 安装有手就行。不折腾就用微 PE 工具箱,需要 Linux 共存就用 Ventoy。然后再能换个内存和硬盘就更好了。我习惯于买小配置然后自己升级,当然土豪随意。手不残党还可以上网搜一搜自己换个笔记本屏幕。
安装软件。远离 360 全家桶,xx 大师,xx 精灵,这些不止流氓,还会带一堆的捆绑软件。杀毒软件就用火绒,或者干脆不装大多数情况也没啥事。还有就是再垃圾软件篡改主页之后再改回来,以及更换浏览器的搜索引擎。
分清楚快捷方式和安装软件的区别。很多人把快捷方式一删除就不管了,很多年前遇到过一个妹子倒腾半天最后发过来一个快捷方式给我。请掌握正确的卸载方法(比如 windows 的控制面板)。还要分清楚安装版和绿色版区别。大家都是这么过来的,windows 虽然不好用,但是会强迫你看到一堆的技术字眼,比如 DLL 链接库, .net 框架等等。
分清无线和有线,以及怎么查看动态 IP 地址和配置静态地址。这能够解决大部分电脑不能上网的情况。如果能自己接网线就更好了。
分清楚电脑接口,比如视频线的 VGA,HDMI,DP, Typec。数据线的 USB2.0 和 3.0,不管 3.X 后面多少后缀,他也是当年的 USB3.0。手边常备一个数据线转接头,转 HDMI 或者 USB。
买电脑买新不买旧,不要为了省钱而受罪。配置太低的可能刷网页,写文档都卡。不需要频繁更新系统。
U 盘不要买金士顿。
不要找程序员修电脑。很多人不会,会的也懒得修。
---
## 使用 MacOS 给泰山派烧录镜像
- URL: https://blog.no-claw.com/posts/89eeed1/
- 日期: 2025-08-08
- 分类: 电子产品 / 纯硬件
- 标签: 硬件
MacOS 单系统单系统一直用了这么多年,最近捡起来硬件,也顺便记录下 MacOS 烧录泰山派 RK3568 的过程。官方教程写得比较简略,踩了几次坑后,整理一下记录,也算是给同样习惯用 Mac 的朋友一个参考。
官方文档参考链接:
泰山派烧录镜像说明(官方 Wiki)
准备工具
嘉立创网盘提供了烧录工具 upgrade_tool,Mac 上直接下载即可。
下载后,记得先给执行权限:
1
chmod +x upgrade_tool
主要参考了这个文章,提供了有效的 Mac 烧录方法:
知乎参考文章
硬件连接
泰山派上电开机,用 Type-C 接口接到 Mac。
⚠️ 一定要用高速数据线,很多 Type-C 线只支持充电,没数据功能,用这种线是可能识别不了设备,就算能够识别只有几兆的速度。烧录镜像通常几个 GB 大小,速度差的线也容易出错。
按住开发板上的 REC 按键(不松手)。
轻按一下 RST 按键(复位),立即松开,但仍然保持 REC 键按下。
此时设备进入 Loader 模式,可以在 Mac 端确认:
1
./upgrade_tool ld
正常输出会显示类似:
1
2
3
Program Log will save in the /Users/xu/upgrade_tool/log/
List of rockusb connected(1)
DevNo=1 Vid=0x2207,Pid=0x350a,LocationID=1 Mode=Loader SerialNo=86b2acaf11e3305
如果能看到 Mode=Loader,就说明设备被正确识别了。
烧录镜像
官方镜像是 update.img,把它放到与工具同一目录下,执行:
1
./upgrade_tool uf update.img
正常会输出烧录进度,比如:
1
2
3
4
5
6
7
8
Program Log will save in the /Users/xu/upgrade_tool/log/
ftruncate: Invalid argument
Loading firmware...
Support Type:RK3568 FW Ver:1.0.00 FW Time:2024-09-19 08:50:15
Loader ver:1.01 Loader Time:2024-09-18 17:38:28
Download Image Total(4957941K),Current(2925601K)
Download Image Total(4957941K),Current(4066337K)
Upgrade firmware ok.
最后出现 Upgrade firmware ok. 就表示烧录完成,可以松开 REC 键了。
总结
整体流程下来,其实在 MacOS 上烧录泰山派并不复杂:
核心是 upgrade_tool 工具 + 正确的键位操作;
数据线质量很关键;
日志里的部分报错不用慌,只要最后有 Upgrade firmware ok. 就算成功。
---
## Mac 上 Orbstack的Docker容器访问微服 Wordpress IPv6 解析问题记录
- URL: https://blog.no-claw.com/posts/2ad973ba/
- 日期: 2025-08-07
- 分类: 懒猫微服 / 排查
- 标签: Docker, 懒猫微服
今天在 Mac 上的 Docker 容器访问微服里的 Wordpress 时,遇到了 IPv6 无法正常访问的问题。
现象是:dig 能解析出 IPv6 地址,但容器内网络不可达。
https://appstore.lazycat.cloud/#/shop/detail/dev.beiyu.wordpress
问题现象
dig 查询正常,能返回 IPv6 结果。
但容器内访问(curl、ping6)失败,提示网络不可达。
原因排查
查询后发现:
默认情况下,Docker 引擎并未为容器分配 IPv6 地址。
这导致虽然 DNS 能解析,但容器无 IPv6 出口。
解决方法
在 Orbstack 设置中开启 IPv6 支持即可:
开启后,Docker 引擎会自动重启。
无需手动添加 --ipv6 启动参数。
验证结果
开启 IPv6 后,在容器内执行 curl 获取 Wordpress RSS 链接,正常返回内容:
1
curl https://micro.heiyu.space/feed
ping6 测试也正常:
1
2
3
4
5
6
[root@5c79a5875d68 easysearch]# ping6 micro.heiyu.space
PING micro.heiyu.space(fc03:1136:384f:313:a637:437:d22b:0) 56 data bytes
64 bytes from fc03:1136:384f:313:a637:437:d22b:0: icmp_seq=1 ttl=62 time=4.27 ms
64 bytes from fc03:1136:384f:313:a637:437:d22b:0: icmp_seq=2 ttl=62 time=5.85 ms
64 bytes from fc03:1136:384f:313:a637:437:d22b:0: icmp_seq=3 ttl=62 time=3.36 ms
64 bytes from fc03:1136:384f:313:a637:437:d22b:0: icmp_seq=4 ttl=62 time=3.97 ms
总结
在 Mac 上运行的 Docker 容器默认不分配 IPv6 地址,需要在 Orbstack 设置中手动开启 IPv6 支持。开启后无需额外配置,容器即可正常解析并访问 IPv6 目标。
---
## 修改 Hexo 的 RSS 阅读数量(Icarus 主题)
- URL: https://blog.no-claw.com/posts/2d0d3922/
- 日期: 2025-08-07
- 分类: 电子产品 / 电脑 / NAS
- 标签: Blog
Icarus 主题本身不直接生成 RSS,只是提供 RSS 链接的展示位置(例如导航栏、侧边栏的 RSS 按钮)。
真正生成 RSS/Atom 的功能,需要依赖 Hexo 的插件(通常是 hexo-generator-feed)。
因此,要修改 RSS 条数,需要分两步:
1. 安装 RSS 生成插件
在博客根目录(_config.yml 所在目录)执行:
1
npm install hexo-generator-feed --save
2. 在站点 _config.yml(根目录)配置 RSS
RSS 条数由站点配置控制,而不是主题配置。示例:
1
2
3
4
feed:
type: atom # 类型,可选 atom、rss2、json
path: atom.xml # 输出文件名
limit: 50 # 输出文章数,0 表示全部文章
如果不写 limit,默认值是 20(插件源码中 var limit = config.limit || 20)。
3. 在 Icarus 主题 _config.yml 添加 RSS 链接
Icarus 主题只是显示你生成好的 RSS 地址,常见的配置方式有两种:
导航栏:
1
2
3
navbar:
menu:
RSS: /atom.xml # 对应 feed.path 的路径
侧边栏(Profile 小部件):
1
2
3
4
5
6
7
widgets:
- position: left
type: profile
social_links:
RSS:
icon: fas fa-rss
url: /atom.xml
4. 重新生成站点
1
hexo clean && hexo g
然后访问:
1
http://你的域名/atom.xml
即可查看 RSS 内容。
优先级说明
RSS 条数生成逻辑 → 由 hexo-generator-feed 插件控制,只读取 站点 _config.yml 的 feed 配置。
RSS 链接展示 → 由主题(如 Icarus)控制,在主题 _config.yml 中设置按钮或链接。
如果两个地方都有 feed: 配置,站点 _config.yml 优先级更高,主题配置不会覆盖它。
---
## 避雷! 百大值友熊猫不是猫公开抄袭文章理直气壮,态度恶劣,投诉、辟谣、关评全流程实录
- URL: https://blog.no-claw.com/posts/951989c3/
- 日期: 2025-08-05
- 分类: 零碎生活 / 零碎
- 标签: 外设
前面的文章提到了 熊猫不是熊猫/panda 不是猫,这个人抄袭我的文章,我出于礼貌跟他沟通,希望他道歉和整改结果没有任何后续,于是我发布了一开始的文章
最早在这个平台上刷到我抄袭我的文章,然后果断投诉下架,值得买和知乎也处理了他的文章。
同时他的公众号也抄袭了这个文章,阅读量有小 1W,转发就有 100 多次,也不清楚到底是赚了多少钱。
更有意思的是,他不仅不道歉而且在公众号上发辟谣。来说不是抄袭之类的。
然后评论区就是一边倒的状态,甚至有说“他就是挂出来挨打的”。
这个是我一开始评论的样子,后来他直接屏蔽我的留言。为了让大家看明白这个事情,所以我贴了我的原文链接。,
过了一天之后发现评论变成了仅作者可见,显然是心虚了,不敢让其他读者看原文。
从工信部的备案中能够查看这个人的信息,还会继续扒其他信息的,也欢迎互联网网友提供。(信息为公开)
抄袭文章,然后连一个公开的道歉都没有么?然后发过来发辟谣说我侮辱他?
简直是素质低劣,屡教不改。我仁至义尽了。
很难不让人认为他其他文章是不是也是抄的,多次沟通让他公开道歉,但是这个态度实在在就是惯犯。
如果有商家想找他合作推广产品,慎重吧。。。
典型的死猪不怕开水烫。
很多的情况是很多原作者流量不高,抄袭的人天南海北的洗,所以流量数字也好看。等被发现或者告了。再来个下架关帐号建新号来逃避法律的追责。如此往复,抄袭者总是能无本获利。我呼吁能够维护原作的知识产权,不要让这种蛀虫危害互联网。
---
## 上网逛逛开源项目,发现自己的文章被偷了,心慈手软给对方整改机会,结果后悔到拍大腿!
- URL: https://blog.no-claw.com/posts/5f056020/
- 日期: 2025-07-31
- 分类: 零碎生活 / 零碎
- 标签: 生活
本来想找点 NAS 的开源项目玩一玩,然后很巧搜到了别人”介绍”我的作品,一开始还挺高兴的,起码自己写的工具有人用了。除了我的朋友们,懒猫微服的圈子之外还有互联网上的人用。然后看到底,这个文字风格咋怎么眼熟,就好像是我的风格,打开我原来的文章,确认是洗稿无疑了。(后面有证据)
事件起因
我的文章最早是发在懒猫微服的,后续也发在了亚马逊云科技的公众号上,这个稿件是商业性质了,所以足够够成侵权了。大家记住这个人,虽然他删了帖子,但是再前前后后与他沟通之后,我没有得到他良好的认错态度,所以还是决定把这个文章发出来。
记住这个 ID,这个人有很多平台,有的叫做 panda 不是猫。然后他所有的平台都洗稿了我这个文章,并且流量还不低。而且我的文章是给懒猫微服的商业供稿,就被这个人无耻的剽窃了。所以也很难不怀疑他的其他文章也是不是洗稿来的。
抄袭证据一览
我列出了整个文章的四个抄袭的点,几乎涵盖了所有的项目介绍,而且介绍顺序,功能,隐藏彩蛋,话术丝毫不差。
抄袭 1: 核心功能描述被整段搬运
我的原文:Containly 的核心功能是通过目录央射的 Docker 引擎读取所有容器信息,包括容器的启动、退出、停止及其他:如,当容器处于“Create“状态时它会被标记为“Other”状态,便于管理,
他的洗稿抄袭版本:界面非常清爽,是我喜欢的类型,整个容器就这么一个界面,能看到容器的启动、退出、停止及其他状态。例如,当容器处于“Create”状态时,它会被标记为“Other”状态,便于管理。
这个 other 状态其实埋下的一个彩蛋,绝对不是可以第一次使用的是能够测出来的。
抄袭 2: 容器卡片设计原封不动
我的原文:默认情况下,每个容器卡片会显容器的网桥信息、端口映射和 URL。默认使用 HTTP 协议,鼠标悬停时,会在右侧显示操作按钮。通过点击这些按钮操作会被保留,再次点击会隐藏,这样子就整个比较美观。
按钮功能包括
停止/启动
重启
查看日志
SSH 进入容器
切换 HTTP/HTTPS
黑名单管理
他的洗稿抄袭版本:容器卡片会显示容器的网桥信息、端口信息以及 URL 链接地址,默认使用 HTTP 协议,可切换到 HTTPS 协议。
当鼠标悬停时,卡片右侧会显示操作按钮,这里提供了容器的启停重启、日志查看、SSH 功能以及黑名单管理功能,再次点击可以隐藏按钮,显得卡片更为美观。
抄袭 3: 域名拼接也照抄
我的原文:此外,Containly 还提供了一个输入框,用户可以输入需要监控的 NAS 域名,面板会自动根据域名和端口拼接成 URI,并存储在 localStorage 中。更进一步,Containly 还支持暗黑模式,提升了用户体验。
他的洗稿抄袭版本:右上角,Containly 提供了一个地址输入框,在这里填入我们 NAS 的域名或者 IP,面板会自动根据域名和端口拼接成 URI,并存储在 localStorage 中。
抄袭 4: 一句不改的 SSH 彩蛋
我的原文:利用面板的 SSH 功能能够直接从面板进去访问容器的 SHELL,不用执行再 docker exec 的命令。
他的洗稿抄袭版本:利用面板的 SSH 功能能够直接从面板进去访问容器的 SHELL,不用执行再 docker exec 的命令。
(原文一个字没改)
把两篇文章扔到 GPT 里,也判断为洗稿。
根据您提供的两篇文章内容,第二篇文章显然是Containly 项目的原创介绍,详细描述了项目的背景、功能、部署方式以及开发过程。第一篇文章则是对第二篇内容的转载,几乎没有进行实质性的修改或添加新信息。因此,第一篇文章属于抄袭行为。
如果您是第一篇文章的作者,建议您对内容进行充分的改写,或者注明原文出处,以避免版权问题。如果您是第二篇文章的作者,您有权要求删除或修改未经授权转载的内容。
沟通 & 维权过程
先是公众号上告诉他涉嫌抄袭洗稿,但是没有任何效果。
然后给我回复了这个,完全没有意识到自己错在哪里。
我于是贴证据给他,然后就被无视了。他用我这个文章流量也不少,看 100 多的转发应该也赚了不少的广告费。
抄袭他人稿件,然后用来盈利。除了微信的平台之外,其他平台也不会有太少的广告费。
适用法律条款 & 赔偿依据
根据著作权法第 10 条、第 52 条第 5 项及第 53 条第 1 项,对方未经许可复制并在信息网络向公众传播我的文章,已构成剽窃及侵权,应承担停止侵害、赔礼道歉并赔偿损失的法律责任。
第五十四条 侵犯著作权或者与著作权有关的权利的,侵权人应当按照权利人的实际损失或者侵权人的违法所得给予赔偿;权利人的实际损失或者侵权人的违法所得难以计算的,由人民法院根据侵权行为的情节,判决给予五百元以上五百万元以下的赔偿。
为制止侵权行为所支付的合理开支,也可以酌情计入赔偿数额
写在最后:给创作者的提醒
那再给一个机会吧,然后加微信聊聊,说不定有悔意呢(实时证明我单纯了)
然后对方一开始没理不饶人,简直认为原创。
给了他半个多周的时间来整改,然后就没有然后了,再也没回过我消息。这种无耻的人,还是把他发出来,让大家避雷吧,这种靠抄袭别人还振振有词骗流量的人,才是当代互联网的蛀虫。
免责声明
本文仅陈述可核实事实,所引用截图均为证据保存;如有异议可联系作者。
这个人微信号在他平台上有,不能够算恶意曝光泄漏隐私。
---
## 找到了一个wedav的项目
- URL: https://blog.no-claw.com/posts/3ebd9b42/
- 日期: 2025-07-29
- 分类: 电子产品 / 电脑 / NAS
- 标签: NAS
win10 连接 Mac 共享的 SMB 有问题,所以想挂载 webdav,于是发现了这个项目:
可以不话心思解决 mac 个 windows 关于 Sambda 的兼容问题。
https://github.com/mar10/wsgidav
WsgiDAV 是一个支持 SSL 的独立 WebDAV 服务器,可以在 Linux、OSX 和 Windows 上作为 Python 命令行脚本运行。它的主要功能包括:
主要功能:
WebDAV 支持:它提供完整的 WebDAV 协议实现,用于通过 HTTP 协议远程访问、管理和编辑文件。
SSL 支持:你可以为 WsgiDAV 启用 SSL 加密,确保文件传输的安全性。
文件系统提供程序:通过文件系统提供程序,允许你将文件夹暴露为 WebDAV 共享。
基本认证和 PAM 登录认证:支持基本认证,并且支持在 Linux 或 OSX 上使用 PAM 认证。
Docker 支持:WsgiDAV 提供了一个实验性的 Docker 镜像,可以在 Docker 容器中运行 WebDAV 服务。
多线程支持:支持高性能的多线程 Web 服务器功能。
安装:
安装 wsgidav 和 cheroot:
1
pip install wsgidav cheroot
启动 WsgiDAV 服务器并启用匿名访问:
1
wsgidav --host=0.0.0.0 --port=80 --root=/tmp --auth=anonymous
配置选项:
--auth=anonymous 启用匿名认证,允许没有身份验证的访问。
--auth=pam-login 启用基于 PAM 的认证(在 Linux 或 OSX 上使用)。
启用 SSL(推荐使用):
1
wsgidav --host=0.0.0.0 --port=8080 --root=/tmp --auth=anonymous --ssl-adapter pyopenssl
用 Docker 启动 WebDAV 服务器:
拉取 Docker 镜像:
1
docker pull mar10/wsgidav
运行 Docker 容器:
1
docker run --rm -it -p 8080:8080 -v /tmp:/var/wsgidav-root mar10/wsgidav
扩展功能:
虚拟文件系统:通过 WebDAV 使数据结构呈现为可编辑的文件系统。
文档编辑:支持在线编辑 MS Office 文档。
集成 WSGI:WsgiDAV 可以作为 WSGI 应用程序在其他 WSGI 兼容的 Web 服务器上运行。
结论:
WsgiDAV 是一个灵活、强大的 WebDAV 解决方案,适合用于文件共享、在线文档编辑等应用场景。如果你需要在 Python 环境中快速部署 WebDAV 服务,它提供了简单的命令行启动选项以及 Docker 支持。如果需要更多自定义功能,还可以通过配置文件和 WSGI 中间件进行扩展。
---
## 使用 OpenList 将 S3 转换为 WebDAV
- URL: https://blog.no-claw.com/posts/21efda6/
- 日期: 2025-07-28
- 分类: 软件 / AWS
- 标签: AWS
Amazon S3 是一种高可扩展、低延迟的对象存储服务,广泛用于存储和管理数据。尽管目前有多个工具来将 S3 与其他存储解决方案(如 Storage Gateway,EMRFS 或者 S3FS 等)集成,但今天我们介绍一个新的方法,通过使用 OpenList,将 S3 存储转换为 WebDAV,简化文件管理和访问。
本文将引导你通过 Docker Compose 启动 OpenList,并将其与 Amazon S3 配置,以便通过 WebDAV 协议进行访问。
步骤 1:使用 Docker Compose 启动 OpenList
首先,我们需要通过 Docker Compose 来启动 OpenList 服务。以下是一个示例 docker-compose.yml 配置文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
services:
openlist:
image: "openlistteam/openlist:latest"
container_name: openlist
volumes:
- "./data:/opt/openlist/data"
ports:
- "5244:5244"
environment:
- PUID=0
- PGID=0
- UMASK=022
restart: unless-stopped
保存该配置后,使用以下命令启动 OpenList 容器:
1
docker-compose up -d
启动成功后,OpenList 的 Web 界面将在端口 5244 上可用。你可以通过浏览器访问 http://localhost:5244 进入管理界面。默认用户名为 admin,初始密码可以通过环境变量设置,或者在容器日志中查看。
我用的是 Orbstack,可以很方便的查看容器日志。如果你使用的是 Docker cli,也可以使用 docker logs 进行查看。
步骤 2:配置 S3 存储
一开始,OpenList 容器没有绑定任何存储,所以页面将显示为空白。此时需要点击右下角的“管理”按钮,进入存储配置界面。
在“存储”选项卡下,选择“对象存储”作为存储类型。
配置挂载路径(例如 /s3),这相当于 Linux 系统中的挂载目录。
输入你的 S3 存储桶的名称、区域和访问密钥。确保使用正确的 S3 endpoint。
我的存储桶位于东京,因此我在配置中使用了 s3.ap-northeast-1.amazonaws.com 作为 endpoint。如果你的存储桶位于其他区域,记得修改为相应区域的 endpoint。
为了方便获取你的 AWS 凭证,可以使用以下命令获取当前机器绑定的凭证:
1
pip install awsx
如果你使用 uv 管理 Python 环境,可以运行以下命令打印当前用户名和使用的 Access Key 以及 Secret Key:
1
uvx awsx
步骤 3:配置重定向
与许多 SDK 的重定向机制不同,如果你在配置中错误地设置了美东区的 endpoint,OpenList 客户端将不会自动在收到 301 重定向响应后转发请求到正确的区域,而是会报错。
例如,如果你将 endpoint 设置为 s3.us-east-1.amazonaws.com,但存储桶位于 ap-northeast-1 区域,你将遇到以下错误:
1
BucketRegionError: incorrect region, the bucket is not in 'ap-northeast-1' region at endpoint 's3.us-east-1.amazonaws.com'
解决方法是确保在配置中使用正确的区域,避免跨区域错误。
步骤 4:启用 MFA(可选)
为了提高安全性,尤其是在将 OpenList 部署到公网环境时,建议启用多重身份验证(MFA)。启用 MFA 可以增加 AWS 账户的安全性,避免潜在的安全风险。
在 AWS 控制台中启用 MFA 后,记得更新 OpenList 中的凭证配置,确保启用了双重认证。
步骤 5:配置用户权限
OpenList 默认情况下将用户权限设置为只读。要赋予 admin 用户 WebDAV 的管理权限,请进入“用户 - 编辑”界面,修改相应的权限设置。
步骤 6:访问 S3 文件
完成配置后,OpenList 将自动同步 S3 存储桶的数据。你可以在 Web 界面上方便地进行文件下载、解压、上传文件等操作。
这个是 S3 上页面,可以看到 s3 的数据都被同步到 Openlist 上了。
同时也能够在 Openlist 上在线观看 S3 上的存的视频教程。
同时所有操作都可以通过 WebDAV 协议进行,访问路径为:
1
http(s):///dav
例如,在 MacOS 上,可以通过 Finder 进行 WebDAV 访问:
在 Finder 中选择“前往”>“连接服务器”。
输入 WebDAV 路径,例如:http://localhost:5244/dav。
输入 OpenList 的用户名和密码进行身份验证。
在 Finder 中使用 WebDAV 进行访问:
你还可以使用 Linux 命令行来操作 WebDAV,减少了学习 S3 命令行的成本。
总结
通过使用 OpenList,我们可以轻松地将 Amazon S3 转换为 WebDAV,简化了文件访问和管理。通过本文的步骤,你可以快速启动 OpenList、配置 S3 存储桶,并通过 WebDAV 协议访问存储在 S3 上的文件。希望这篇文章能帮助你更高效地管理 S3 数据,并为你提供更加便捷的文件访问方式。
---
## 再也不用拼手速!一条命令直接进 BIOS(Windows/Linux/macOS 全攻略)
- URL: https://blog.no-claw.com/posts/a0b67926/
- 日期: 2025-07-27
- 分类: 电子产品 / 电脑
- 标签: 外设
看了大狸子的教程, 尝试了一下,顺便补充了其他平台的命令。
很多朋友在装系统、改启动项、开虚拟化(VT-x)、关闭安全启动的时候,都需要进入 BIOS/UEFI。但平时开机要拼命按 DEL/F2/F10/ESC 等快捷键,手速还要够快,稍微慢一点就错过了。
其实在 Windows 和 Linux 系统里,都有“命令行直达 BIOS”的办法,简单又优雅。今天给大家写个全攻略。
一、Windows 系统
1. 命令行进入 BIOS
在 Windows 10/11 打开 命令提示符(管理员) 或 PowerShell(管理员),输入:
1
shutdown /r /fw /t 0
参数说明:
/r → 重启
/fw → 重启后进入 BIOS/UEFI 固件设置
/t 0 → 立即执行(默认是 30 秒倒计时)
执行后,电脑会立刻重启并直接进入 BIOS。
⚠️ 注意:
必须是 UEFI 启动模式才支持 /fw,如果是 Legacy BIOS 会报错 “找不到环境选项(203)”。
可以在命令行里输入 msinfo32,检查 “BIOS 模式” 是否为 UEFI。
2. 系统设置进入
如果命令报错,可以这样操作:
打开 设置 → 更新和安全 → 恢复
在 “高级启动” 点击 立即重新启动
依次选择 疑难解答 → 高级选项 → UEFI 固件设置 → 重启
二、Linux 系统
Linux 用户也能一键进入固件设置:
1
systemctl reboot --firmware-setup
这个命令支持大多数基于 systemd 的发行版(Ubuntu、Debian、Fedora、Arch 等)。
如果提示不支持,那就只能用开机热键进入。
三、macOS 系统
Mac 没有传统 BIOS,只有 EFI 设置。
Intel Mac:
开机时长按 Option (⌥) 进入启动磁盘选择界面。
长按 Command + Option + P + R → 重置 NVRAM/PRAM,相当于恢复固件设置。
**Apple Silicon (M1/M2/M3)**:
完全关机
长按 电源键,直到出现“启动选项”
点击 “选项” 进入恢复模式,在里面可以修改启动安全策略等。
四、常见品牌 BIOS 快捷键速查表
品牌
快捷键
联想 ThinkPad
F1
联想 IdeaPad
F2
华硕 ASUS
F2 或 DEL
惠普 HP
ESC 或 F10
戴尔 Dell
F2
宏碁 Acer
F2 或 DEL
微星 MSI
DEL
技嘉 GIGABYTE
DEL
微软 Surface
音量加 + 电源键
总结
Windows 用户:最推荐 shutdown /r /fw /t 0,前提是 UEFI 模式。
Linux 用户:推荐 systemctl reboot --firmware-setup。
Mac 用户:通过开机组合键进入固件设置。
以后再也不用拼手速狂按 F2/DEL 了,直接用命令行一键进 BIOS,优雅又高效。
---
## TrackWeight, 把 Macbook 的触控板当作电子秤
- URL: https://blog.no-claw.com/posts/10473fce/
- 日期: 2025-07-27
- 分类: 电子产品 / 电脑
- 标签: MacOS
大数据推送的,地址在这里 https://github.com/KrishKrosh/TrackWeight/
最新的 release 打开一开始是这样,
然后给作者留言,修复了 MacOS13 上不能运行的问题。(不到 10 分钟作者就改好了 release)
这个程序需要手指始终触摸板,然后一开始需要校准。
校准之后就可以放置物品了。
除了手有点抖之外,总体还不错。
这个页面也很好看,
---
## 记一次iphone的死机
- URL: https://blog.no-claw.com/posts/b81a6bca/
- 日期: 2025-07-27
- 分类: 电子产品 / 电脑
- 标签: 手机, Apple
早上起来发个微信,拍照片,然后 IOS 就死机了。
能正常推送 app 通知,电话也能正常打进来。表面现象是屏幕失灵,主板功能正常。
路人给出的排查:
手机内屏坏了
内屏触点松了
屏幕积液
到了 apple 线下直接强制重启,按一下+, 再按一下- ,然后长按电源键。然后重启解决了。目测是没有硬件问题,只是 IOS 的崩溃。iphone13 也不能再更新系统了。
我也用 GPT 搜到了这个步骤,只是需要按压 20 秒,我按的时间不够。
你遇到了这个问题,可以长按超过 20 秒来重启。
---
## AWS Credit 充值到账户全流程图文指南
- URL: https://blog.no-claw.com/posts/11273243/
- 日期: 2025-07-25
- 分类: 电子产品
- 标签: AWS
在云计算的世界里,AWS Credit(代金券) 就像是给大家的一张“云上优惠券”。很多朋友可能是在参加 黑客松、社区活动、竞赛 时拿到的,也有的是通过 AWS Educate、Activate 初创企业计划 领取的。无论来源如何,这些 Credit 都能在你使用 AWS 服务时抵扣费用,帮大家节省真金白银。
但是,Credit 拿到手之后,如何把它“充值”到自己的 AWS 账户里?下面我结合实际操作截图,为大家做一个完整的图文教程。
一、进入 AWS Billing 控制台
首先,登录 AWS 管理控制台,在搜索栏里输入 Billing。
进入 Billing Dashboard,就能看到当前账户的费用情况,包括账单、支付方式和 Credit 使用情况。
👉 示例截图如下:
在这个总览页面,大家可以随时掌握自己账户的消费情况。
二、查看账户已绑定的 Credits
接着,在左侧菜单栏找到 Credits,进入代金券管理页面。
这里会列出所有已经绑定到你账户的 Credit,包括:
金额(Amount)
有效期(Expiration Date)
剩余可用额度(Remaining Balance)
三、兑换新的 Credit
如果你手上有 AWS 发放的 Promotion Code(兑换码),就可以在这里点击 兑换积分,输入兑换码完成绑定。
👉 输入兑换码示例:(我这个是刚刚兑换完)
点击 Redeem 后,系统会提示你“成功绑定”,这就说明 Credit 已经充值到账户啦!
四、验证充值是否成功
绑定成功后,再回到 Credits 页面,就能看到新的 Credit 已经显示出来。
接下来,AWS 在结算账单时会 优先从 Credit 中抵扣,只有当 Credit 用尽或过期,才会从银行卡/信用卡中实际扣款。
这意味着,只要你有 Credit,账户就能“免费”使用 AWS 服务一段时间。
五、常见问题解答
Credit 可以转到其他账户吗?
不行,AWS Credit 只能充值到指定的账户,无法转让。
哪些服务可以用 Credit 抵扣?
大部分 AWS 原生服务(如 EC2、S3、RDS、Lambda)都支持。部分 Marketplace 第三方产品 可能无法使用。
Credit 会不会过期?
会的!每个 Credit 都有有效期,过期之后就无法使用。所以大家要注意在有效期内消耗掉。
扣费顺序是怎样的?
系统会优先消耗 Credit,Credit 用尽后才会从绑定的支付方式扣款。
六、总结
整体流程其实非常简单:
👉 登录 Billing Console → Credits 页面 → Redeem credit → 输入兑换码 → 验证到账。
这样,你就能安心使用 AWS 服务,账单会自动优先从 Credit 扣除,大大节省云上的支出。
七、使用场景举例
学生开发者用 Educate Credit 免费部署个人网站;
初创团队通过 Activate Credit 在 AWS 上搭建 MVP,降低前期成本;
参加黑客松拿到的 Credit,用来训练 AI 模型或做大数据实验。
对个人和企业来说,合理使用 AWS Credit,能帮助大家 快速试错、降低成本、加速创新。
🔥 小结一句:AWS Credit 不仅是一张优惠券,更是你云上实验和创新的“启动资金”。
---
## 家居常用测网速软件
- URL: https://blog.no-claw.com/posts/73f93987/
- 日期: 2025-07-25
- 分类: 电子产品 / 外设
- 标签: 外设
中兴路由器测速的软件
speedtest 和中科大测速有时候抽风,中兴自带的还不错
手机也可以使用全球网测,
安卓使用花瓣测速
---
## 懒猫微服实战入门(二十四):在懒猫微服中使用群晖
- URL: https://blog.no-claw.com/posts/815f771a/
- 日期: 2025-07-25
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服, NAS, 群晖
各种 OS in docker 的操作很多,包括 debian,arch,windows,Macos,安卓,这次甚至带来了黑群晖 in docker,基本上都是基于 QUME 的虚拟化来做的。也希望哪天有也有 docker 版本的 QNAP 吧。NAS 一家人就要整整齐齐。(unraid 估计没戏 哈哈哈哈哈)
https://appstore.lazycat.cloud/#/shop/detail/chestnut.app.vdsm
从懒猫微服商店就可以直接下载了,群友上传了 release。
开源地址在这里:https://github.com/vdsm/virtual-dsm
也不需要再搞群晖引导啥的,直接一件安装很方便,最后设置下用户名和密码就可以了。
版本是目前最新的 DSM7.22,相信群晖应该是很多爱好者的第一个 NAS 系统。这些年我一路从 DSM6 走过来,群晖的系统 UI 是越来越好看了。第一件是必须是安装套件,激活文件管理器。
美中不足的是,没有找到群晖的虚拟机管理器。也罢,反正是虚拟机,VM in VM 性能损耗很大,倒是 Docker in Docker 还能接受。
虽然这个存储空间只有 16G,不过嘛,感觉这个大小其实适合尝鲜或者做已有群晖的 backup。不过机械硬盘倒是没有明显的卡顿。
去应用查看器翻了一下,磁盘大小竟然是在环境变量写死的,那么在 V1.38+的 OS 也能通过修改环境变量来换成更大的空间,毕竟数据盘都是 HDD 不差这点空间。
因为我已经有了一个物理机的群晖,上面也跑着虚拟机和打印机驱动的一些软件,所以这个群晖更多是尝鲜,或者说组成一个集群双活。磁盘太小的话,那么我就添加外部的 SMB,其实就是懒猫网盘。
在群晖的文件管理器中 - 工具 - 装载远程文件夹 - CIFS 共享文件夹。
然后输入 SMB 的信息。需要事先选中一个空文件夹。
这个时候发挥 Linux 的 mount 哲学了 hhhhh
然后把群晖自己 SMB 映射出去,还能做一个 SMB 的存储网关。
懒猫网盘和 share 都是 SMB 挂载的其他设备。也能通过这个虚拟群晖的 SMB 一起访问。
怎么洗白?都有懒猫微服穿透和相册了?不用洗了吧!
---
## 懒猫微服炫技篇(三):sunshine+moonlight 双人串流打游戏
- URL: https://blog.no-claw.com/posts/2d63e359/
- 日期: 2025-07-22
- 分类: 懒猫微服 / 炫技
- 标签: 懒猫微服
之前在懒猫商店上架了坦克大战,一直想着怎么双人一起玩的问题。现在的年轻人除了逢年过节之外很难凑在一起,所以就想着能不能通过远程共享+键盘映射的办法远程双人游戏呢?碰巧刷到了司波图的 NAS 串流打游戏视频,加上之前一直被朋友安利 sunshine+moonlight 的组合,远程 linux 比 VNC 和 XRDP 都要好很多。所以我也来试试。
下载地址:https://app.lizardbyte.dev/Sunshine/?lng=zh-CN
懒猫微服默认没有图形化,所以直接安装串流服务端也没有意义。于是我用懒猫开启了一个 Windows 虚拟机来做服务器,关于后面怎么安装 windows 虚拟机,后面来讲,或者你也可以找一台物理机 windows 来安装懒猫微服的客户端。
然后把 windows 作为串流的服务端(安装 sunshine)。直接 EXE 安装,然后启动之后在浏览器中设置一下串流的密码就好。
为什么不用 RDP?
RDP 的原生限制,想两个人连接同一个屏幕操作,需要改注册表,很麻烦,所以放弃了这个方案。
客户端下载 Monnlight,然后会自动发现局域网设备。(如果是广域网可以考虑组网,朋友之前有试过也可以串流玩 stream)
客户端加入的时候可以使用 PIN 认证,在服务端输入客户端认证弹出的 PIN,然后再输入设备名称。
然后在客户端点击 Desktop 就可以了,第二个设备也可以重复这个操作,实测不会把第一个串流的 Session 挤掉。
我的目的就是用服务器打开懒猫微服的网页,然后两个客户端同时串流玩双人的游戏。
如果是大型游戏的话,可以直接直接从商店下载部署版本,但是这种小游戏还想娱乐一下的,两个人用串流一起玩还是听方便的。
能够看到我用 Ipad 和 Macbook pro 同时串流懒猫微服里的坦克大战,玩双人游戏还可以基本没啥问题的。(只是稍微有点点卡)
也算是圆了一个梦吧。
---
## 懒猫微服实战入门(二十三):使用 GitHub 单点登录到 Memos
- URL: https://blog.no-claw.com/posts/b10cd1f4/
- 日期: 2025-07-22
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服
之前我们介绍了如何使用 Memos 替代 Github,那这次我们来进阶一下,给 Memos 添加 Github 单点登录,如果你开发的应用后续想接入 Github 也可以采取这种办法。
一、创建 GitHub OAuth 应用
1. 打开 GitHub OAuth 应用配置入口
登录 GitHub,点击右上角头像,选择 Settings(设置)
在左侧栏选择 Developer settings
点击 OAuth Apps,然后点击右侧的 New OAuth App 创建新的应用
📷 页面示意:
2. 填写应用基本信息
在创建页面中填写如下字段:
Application Name:如 Console SSO
Homepage URL:建议填写系统主页,例如 https://memos.name.heiyu.space
Authorization callback URL:授权成功后的回调地址,格式如下:
1
https://memos.name.heiyu.space/auth/callback
📷 示例填写界面:
3. 获取 Client ID 和 Client Secret
提交后,GitHub 将生成:
Client ID:用于标识该 OAuth 应用
Client Secret:用于身份验证,请妥善保存,不要泄露!
📷 凭证界面如下:
二、查看和管理 OAuth 应用
创建完成后,返回 OAuth 应用列表,即可看到刚创建的应用。
点击应用名可查看授权信息和应用详情:
📷 应用列表和详情视图:
应用列表页面
然后邮件会收到 Github 绑定 Oauth 的通知。
4. 在 Memos 中配置 GitHub 登录
进入系统设置页面:
点击左下角齿轮图标(设置)
选择 单点登录(SSO)
选择 GitHub 作为登录方式,填写刚获取的 Client ID 和 Client Secret
📷 Memos 配置页面示例:
配置保存后,注销当前账号,登录页面会显示 GitHub 登录按钮。
📷 登录页面展示效果:
通过以上步骤,我们完成了 GitHub 登录的接入流程:
✅ 创建并配置 GitHub OAuth 应用
✅ 获取并填入凭证
✅ 在 Memos 中启用 OAuth 登录
最后成员列表一览:
---
## 懒猫微服买硬件送服务,刚学的计算机知识顺便接了个单
- URL: https://blog.no-claw.com/posts/cb1ae078/
- 日期: 2025-07-18
- 分类: 懒猫微服 / 番外
- 标签: 懒猫微服, Cloudflared
事先声明,懒猫微服不提供接单服务,但是可以通过贡献攻略和移植应用赚取激励。
挺有意思的一个事,头几天刚刚找过懒猫微服的技术帮我配置 cloudflare 相关操作,顺便学习了一下基本使用,把自己在 AWS 的 Route53 上购买的域名迁移过去了,然后代理到了博客,AWS EC2 服务器,甚至家里的机器。
偶然间在微信群看到这样一个需求,这不就是前两天懒猫微服的技术人员手把手教我做的。cloudflare 有很多操作,之前周围的人还有使用 cloudflare 反向代理到家里的 NAS,然后 obsidian 实时同步笔记连回家的。
于是我给了他三个方案:
最省心的:cloudflare 只做域名解析,应用无缝迁移到 Linux 服务器上。
全托管的:cloudflare 做域名解析,然后利用 cloudflare 的 Worker 部署后端
最日常的:使用 cloudflare tunnel 反向代理到家里电脑。
客户选择了方案 3,然后接下来就是配置 cloudflare tunnel,甚至不用再配置 A 记录。
🌐 Cloudflare Tunnel 能干什么?
把本地 Web 服务(如网站、应用、API、NAS)通过 Cloudflare 安全暴露到公网
无需公网 IP,不管你是在家庭宽带、NAT、内网还是 IPv6-only 网络都能跑
通过 Cloudflare 的全球 CDN 加速和防护(DDoS 保护、TLS、WAF)
支持访问控制(如 Zero Trust)
支持反向代理多服务(如 /app1, /app2)或多个子域名绑定
首先登录到https://dash.cloudflare.com/首页。点击Zero-Trust。
然后选择 网络 - Tunnels ,然后新建隧道来内网穿透。
选择创建隧道,这个哥们是 Windows 的环境,所以隧道类型使用 Cloudflared。
然后选择新建隧道,然后输入隧道名称。
这个时候选择安装 cloudflared 引擎,需要安装一个 agent,基本是全平台都有,甚至还有 Docker 版本的。
然后把 test 子域代理本地的 localhost:8000。
然后通过域名访问就可以了。
这哥们还有一个额外的要求,要开启启动天 Cloudflared ,然后 GPT 了一下
直接启动已安装的 Cloudflared 服务
运行 Start-Service cloudflared 启动服务
使用 Get-Service cloudflared 查看服务状态
设为开机自启
运行 Set-Service cloudflared -StartupType Automatic 将
Cloudflared 设置为自动启动
结语
买 NAS 学的是网络技术,虽然可能是别人眼中的野路子。但是多一分趣味嘛。切身感受到技术的意义。
---
## GIT小书
- URL: https://blog.no-claw.com/posts/fa9f211a/
- 日期: 2025-07-17
- 分类: 软件
- 标签: GIT
第一章:不明觉厉的 Git
小李刚从大学毕业,加入了一家快速发展的初创公司,成为了公司的前端开发工程师。这是他人生中的第一份正式工作,他兴奋又忐忑。虽然他在学校里学过一些编程技术,但真正的项目经验还很薄弱。第一天,老板就把他分配到了一个正在开发的 Web 项目中,需要用 Git 进行版本管理。
“小李,这是你需要参与的项目,我们已经把代码推到 GitHub 上了,记得拉取下来工作。”老板简短的几句话让小李有点懵。
“GitHub?拉取?我听说过 Git,但从来没用过。”小李在心里嘀咕着。他记得在学校的课堂上,老师提到过 Git 作为一种版本控制工具,可以帮助开发团队协作,但具体怎么使用,还是个谜。
“小李,别担心,我们的团队里有很多 Git 使用经验丰富的人,你可以请教他们。”老板似乎察觉到他的一丝不安,轻轻拍了拍他的肩膀。“首先,你得把代码克隆到本地。”
“克隆?那是什么?”小李心中更是一阵迷茫。
他的同事小王看出了他的困惑,走过来笑着解释:“Git 是一种分布式的版本控制系统,‘克隆’是从远程仓库复制一份代码到你本地电脑上的操作。你只需要使用 git clone 命令,把我们的仓库拉下来就行了。”
小李点点头,拿起电脑,打开命令行,准备开始他的 Git 之旅。
第二章:第一次克隆
小李根据小王的提示,打开了 GitHub,找到了公司项目的仓库链接。接着,他按照小王的指示,输入了以下命令:
1
git clone https://github.com/company/project-repo.git
他按下回车键,屏幕上出现了下载的进度条,Git 开始从远程仓库将项目文件拉取到本地。当下载完成后,他看到自己的本地目录下多了一个与仓库同名的文件夹,这时,他才恍若大悟,原来 Git 仓库就像是一个储藏库,而 git clone 命令就是让这个储藏库的内容变成了他自己本地的副本。
“小王,这样我就能开始写代码了吗?”小李兴奋地问。
“是的!不过在你修改代码之前,你需要查看一下当前的状态。” 小王笑了笑,接着说:“输入 git status 命令,它会告诉你当前文件夹里的文件是否已被 Git 跟踪,是否有变更。”
小李按照提示输入了 git status,屏幕上显示出一长串信息,告诉他哪些文件被修改了,哪些文件没有被 Git 跟踪。
“原来 Git 会这么细致地记录每个文件的变化啊!太厉害了。” 小李感慨道。
第三章:第一次提交
经过短暂的适应,小李开始修改代码。他添加了一些新的功能,并修复了一个小 bug。这时,他想把自己做的更改提交到 Git。
“小王,接下来该怎么办?”小李再次求助于小王。
“你需要先使用 git add 把修改的文件放到暂存区,然后再用 git commit 提交到本地仓库。” 小王耐心地解释道。
小李按照步骤执行:
1
2
git add index.html
git commit -m "修复首页 bug,添加登录功能"
“提交完成了!” 小李兴奋地说道。
小王接着补充道:“记住,提交信息要简洁明了,别人可以通过这些信息快速了解你修改的内容。”
小李点点头,觉得这比学校的作业提交要简单多了。每一次修改都能被记录下来,每一次提交都可以清晰地说明自己做了什么。
第四章:第一次推送
小李第一次提交到本地仓库后,心里有了些许成就感。接着,他又向小王询问:“如果我想把本地的修改推送到远程仓库,应该怎么做?”
“你需要用 git push 命令来推送你的提交到远程仓库。” 小王笑着回答,“不过,在推送之前,你需要先从远程仓库拉取最新的代码,避免和其他人的修改发生冲突。”
“哦,明白了。”小李迅速输入了:
1
2
git pull
git push origin main
当他成功将本地修改推送到远程仓库时,屏幕上出现了“push successful”的提示。他心中不禁涌现出一股自豪感:“原来,Git 还真是方便,和团队合作时,每个人都能在同一个项目上协同工作!”
第五章:第一次遇到冲突
几天后,团队里另外一位开发者小张修改了同一个文件,并且推送到了远程仓库。小李接着更新了代码库,准备继续开发时,突然遇到了一个问题。
“Git 说我无法推送,提示我当前分支落后于远程分支。”小李看着终端输出的错误信息,感到有些困惑。
小王走过来看了一眼,解释道:“这是因为你在推送之前没有拉取最新的远程代码,Git 检测到远程仓库有你没有更新的提交,因此推送失败。”
“那该怎么办?”小李焦急地问。
“我们需要先拉取远程的更新,解决可能的冲突,再进行推送。”小王平静地说道。
于是,小李输入了:
1
git pull origin main
Git 检测到有冲突文件后,小李被要求手动解决冲突。经过一番调试,他成功解决了冲突,并将自己的更改再次推送到远程仓库。
“解决冲突是 Git 使用中的一项重要技能,虽然有点麻烦,但熟悉了之后就能游刃有余。” 小王笑着提醒他。
第六章:Git,成了朋友
这一天,小李结束了一天的工作。他站在公司楼下,深吸一口气,觉得整个世界变得更加清晰。Git,这个曾经陌生又让他感到害怕的工具,现在已经变成了他开发工作中不可或缺的伙伴。
“小王,谢谢你教我这么多。” 小李感激地说。
“不用谢,我们都是一个团队。” 小王微笑着回答。
随着日子的推移,小李对 Git 的理解越来越深入。每当遇到问题时,他不再感到焦虑,而是学会了从容应对。在这条开发道路上,Git 已经成为了他忠实的伙伴,帮助他高效管理版本,协同开发。
第七章:第一次的 Git Rebase
随着项目的不断发展,小李逐渐熟悉了 Git 的基本操作,代码管理变得越来越得心应手。然而,随着开发人员数量的增加,项目中的提交历史开始显得越来越凌乱,尤其是一些多次合并的提交记录,看上去非常混乱。
有一天,小李在查看 Git 提交历史时,发现每次合并分支的提交记录都让历史显得很复杂:“这样下去,历史会越来越乱,别人查找问题的时候会很麻烦。”
小李向小王请教,是否有办法将这些杂乱的提交历史整理得更加简洁。
“你可以使用 git rebase 来整理提交历史,” 小王解释道,“git rebase 可以把你的提交历史进行重新排列,将一些不必要的合并提交压缩成更简洁的历史记录。”
小李兴奋地想试一试。于是,他在 Git 上执行了 git rebase -i 命令,进入了交互式 rebase 模式。这个命令让他可以查看最近的几个提交记录,并选择哪些提交需要保留,哪些提交需要合并。
1
git rebase -i HEAD~5
Git 打开了一个编辑器,列出了最近的 5 次提交记录。小李看到,之前的一些功能开发提交被频繁地合并,而这些合并的记录并没有特别的意义。他决定将这些不重要的提交进行合并。
通过将不必要的提交标记为 squash(合并),小李简洁地将历史整理得更加简洁,并减少了重复的合并提交。整理完成后,他使用 git push 推送了这些更改。
当他看到远程仓库的提交历史变得清晰简洁时,心中充满了成就感。原来,Git 不仅是一个强大的版本控制工具,它也能帮助开发者将代码历史整理得井井有条。
第八章:团队合作中的 Git Stash
团队开发中,小李和其他同事经常需要在不同的任务之间切换。一天,小李在开发一个新功能时,突然接到紧急任务,要求他修复一个线上 bug。他立即决定中止当前工作,切换到 bug 修复任务。
“小王,怎么才能保证我现在的工作不会丢失呢?”小李问道。
“你可以使用 git stash 命令,把当前未完成的工作保存起来,等你修复完 bug 后再恢复。” 小王笑着答道。
“这可以让我暂时保存当前的工作进展?”小李眼前一亮。
于是,小李按照小王的建议,输入了以下命令:
1
git stash
Git 将他当前工作区的更改暂时保存了起来,并恢复了工作目录的干净状态。小李随后切换到了修复 bug 的任务,并快速解决了问题。
几小时后,他回到原先的任务时,使用 git stash apply 恢复了之前未完成的工作。
1
git stash apply
小李惊讶地发现,所有的更改和修改都完好无损地恢复了过来。通过 git stash,他不仅没有丢失任何代码,而且能够在不提交的情况下,顺利切换任务。
这让他深刻体会到,Git 是多么强大的工具,在团队协作和多任务处理中,它能够帮助开发者高效管理代码和进度。
第九章:深入 Git Bisect 查找 Bug
项目中的一个新功能上线后,客户突然反馈了一个 bug,导致页面无法正确显示数据。小李接到任务后,立即开始调查这个问题。但问题是,线上代码已经迭代了好几个版本,谁也不清楚到底是哪一次提交引入了问题。
“小王,如何才能定位到具体是哪个提交引入了 bug 呢?”小李问道。
“你可以使用 git bisect 来进行二分查找。”小王回答道,“git bisect 可以帮助你快速找到 bug 引发的提交。它会将历史提交分成两部分,每次告诉你一个范围,你只需要标记 bug 是否存在,Git 会逐步缩小范围,直到找到问题的根源。”
小李恍然大悟。于是,他开始使用 git bisect 查找 bug 的根源:
1
2
3
git bisect start
git bisect bad
git bisect good v1.0
git bisect 会从最近的提交开始,将所有的提交历史分成两部分,并让小李检查每一部分是否存在 bug。每次,他都根据测试结果输入“good”或“bad”,Git 会根据他的反馈继续缩小范围。
最终,Git 在经过几轮查找后,成功定位到某个特定的提交,这个提交引入了 bug。小李修复了 bug 后,通过 git bisect reset 重置了 bisect 状态,返回到正常的开发流程。
“原来 Git 不仅可以帮助我们管理版本,还能高效地找出问题的根源。” 小李感叹道。
第十章:Git 冲突中的成长
随着团队中成员越来越多,开发中出现的合并冲突也越来越频繁。虽然小李已经掌握了一些 Git 的基本操作,但每当遇到合并冲突时,他还是感到有些紧张和不知所措。
一天,他在合并一个分支时,遇到了一个棘手的冲突,Git 无法自动合并他和其他同事的更改。
“小王,怎么解决合并冲突呢?”小李有些焦虑。
小王笑了笑,走过来耐心地解释道:“Git 会标记出冲突的地方,你需要打开冲突文件,手动选择保留哪部分代码。解决完冲突后,再执行 git add 和 git commit。”
小李按照指示,打开了冲突文件,看到了被 Git 标记出来的冲突部分。经过仔细的分析,他决定保留自己的修改,并删除了无关的部分。
解决冲突后,小李使用了以下命令:
1
2
git add .
git commit -m "解决合并冲突"
“解决了!这次我终于顺利地解决了冲突。”小李长舒一口气。
“别担心,冲突在团队开发中很常见,解决起来也是一种成长。”小王鼓励他说。
从这次经历后,小李对 Git 的理解更加深刻,他开始不再畏惧合并冲突,反而在解决冲突的过程中积累了更多的经验。
第十一章:Git 的复合技能——远程协作中的“Push”与“Pull”
时间过得飞快,随着项目的逐步推进,小李越来越熟悉了 Git 的使用。一次,团队的新需求要求他和小张一起共同开发一个新功能。项目中由于要对数据接口进行修改,涉及到的代码文件较多,这也意味着双方必须频繁地同步代码,避免重复工作。
“小李,你负责前端页面的改动,我负责后端接口的修改。我们需要保持代码同步,你把你的修改推送到 Git 仓库,我拉取下来进行合并。” 小张提醒道。
“好的,小张,我会尽量减少冲突的。” 小李点点头,心里想着如何避免两个开发者在代码中的修改冲突。
小李根据约定,开始在本地开发工作并修改了部分前端代码。当修改完毕后,他通过 git add 和 git commit 提交了自己的修改。接着,他输入了 git push 命令,将本地的更改推送到了远程仓库:
1
git push origin feature-frontend
随着“push successful”的提示,修改成功上传到了远程仓库。小李心里松了一口气,开始等着小张拉取他的代码进行合并。
然而,小张这边也在进行着自己的开发工作,修改了后端接口,并且同样进行了提交。当小张准备将修改推送到仓库时,却遇到了困难。“小李,我拉取了你的代码,但是推送时遇到了错误,说我本地分支落后于远程分支。”
“哦,那是因为我在你推送之前就已经提交了我的修改。你需要先拉取我的更改,再进行推送。” 小李知道问题所在,告诉了小张解决方案。
小张理解地点点头,输入了 git pull 来拉取最新的修改:
1
git pull origin feature-frontend
Git 自动将小张的修改和小李的修改进行了合并,并解决了没有冲突的部分。小张再一次执行 git push,这次成功了。
“解决了!感谢你,小李。” 小张松了口气,“这就是协作开发的魅力,不同的代码能在 Git 中无缝连接。”
小李微笑着点头,虽然有时遇到一些小麻烦,但通过 Git,这一切变得简单而高效。在远程协作中,git push 和 git pull 成为他日常开发的两大法宝,每次与团队成员一起开发时,他都能高效同步、解决冲突,确保项目的顺利推进。
第十二章:Git 中的 “Tag” 之旅
随着项目的不断进展,小李和团队的开发进度也越来越顺利。为了标记项目中的一些关键版本,项目经理提出了一个新的要求:“我们需要为每个阶段的发布版本创建一个 tag,便于后续的版本管理。”
“tag,我之前只听说过,但是不太明白具体怎么用。” 小李略显困惑。
“没问题!” 项目经理一边解释一边操作,“tag 就是给某个特定的提交添加一个标签。它通常用来标记版本号,例如 v1.0、v1.1 等。你可以通过 git tag 命令为某个提交打标签。”
小李立刻打开了终端,输入了如下命令,为当前版本打上了标签:
1
git tag v1.0
他通过 git tag 命令查看了所有的标签:
1
git tag
“tag 是不可变的,它就像是一个时间戳,标记了某个关键时刻的版本。” 项目经理接着说,“如果以后想要回到某个版本,可以通过 git checkout 切换到这个标签。”
小李按照提示,通过 git checkout 轻松地切换到标记为 v1.0 的版本,查看了之前提交的代码,并发现 tag 真的非常方便,帮助他回溯项目的重要节点。
每当开发到一个新阶段或发布一个新版本时,小李都习惯性地为当前版本添加一个 tag,以便将来能够快速回顾项目的重要里程碑。
第十三章:Git 在 “临时” 工作中的灵活运用
有一天,小李接到一个紧急任务,需要修复生产环境中的一个 bug。由于 bug 可能影响整个系统的稳定性,他必须马上开始处理,但又不想打乱当前正在开发的其他功能。
“小王,我在本地修改的功能还没有完成,怎么办才能保证现在的修改不会丢失?” 小李有些焦急。
小王走过来,笑着告诉小李:“你可以用 git stash 把当前修改暂时存起来,等你解决完 bug 再恢复。这样就能保证你现在的工作不会丢失。”
小李恍然大悟:“哦,原来可以这么灵活处理!”
于是,他输入了:
1
git stash
Git 会暂时保存小李当前的修改,并将工作目录恢复到干净状态。他迅速切换到修复 bug 的任务中,解决了线上问题。当任务完成后,他输入 git stash apply 恢复了之前未完成的功能开发。
1
git stash apply
“小王,你看,修改恢复了!不管处理什么任务,Git 总能让我保持高效!”小李激动地说。
小王笑着点点头:“是的,Git 不仅帮助我们管理版本,还能在繁忙的开发过程中高效地切换任务。Git 的 stash 让你随时可以保存当前进度,恢复工作,不会有任何遗漏。”
第十四章:Git Revert——从失败中学习
有一次,小李在开发过程中进行了一个重大的功能更新,然而在提交后,他很快发现这个功能并没有按预期工作,甚至还引发了其他问题。为了修复这个问题,他需要撤回这次提交。
“小王,我不小心提交了一个有问题的功能,这个提交需要撤回,怎么操作?” 小李有些焦急地问道。
小王没有惊讶,而是轻松地告诉小李:“你可以使用 git revert 来撤回指定的提交,它会创建一个新的提交来撤销之前的更改,而不会影响历史。”
小李跟着小王的步骤输入了:
1
git revert
Git 创建了一个新的提交,撤销了原先的修改。当他查看提交历史时,发现撤销操作被正确记录了,而原来的错误提交没有影响到后续的工作。
“原来,撤销错误提交也能这么优雅地操作。” 小李松了口气。
“Git 的 revert 命令让你可以安全地撤回更改,并在代码历史中留下清晰的记录。” 小王总结道。
第十五章:Git 中的 “Fork” 和 “Pull Request”
随着项目越来越庞大,团队成员的增多,开发方式也发生了变化。为了更好地管理开发流程,项目经理决定采用 GitHub 的 Fork 和 Pull Request 工作流,以便团队成员能在自己的分支上开发功能,并通过 Pull Request 将修改提交到主仓库。
“小李,今天我给你分配了一个任务,你需要在 GitHub 上对项目进行一些优化。你可以直接 Fork 这个仓库,然后在自己的仓库中开发。”项目经理说道。
“好的,Fork 是什么意思?”小李有些疑惑。
“Fork 就是将原始仓库的完整副本复制到你的 GitHub 账户下,之后你就可以在自己的副本上进行开发了。开发完成后,提交一个 Pull Request,把你的修改合并到主仓库里。” 项目经理耐心解释道。
“明白了!那我就去 Fork 一下。” 小李立刻在 GitHub 上点击了仓库页面的 Fork 按钮,将仓库复制到了自己的 GitHub 账户中。
接着,小李通过 git clone 克隆了自己的仓库到本地:
1
git clone https://github.com/your-username/project-repo.git
然后,他在自己的仓库中开始进行代码的修改。修改完成后,他通过 git push 将自己的更改推送到了自己的 GitHub 仓库,并创建了一个 Pull Request 请求将更改合并到主仓库。
1
git push origin feature-optimization
小李完成了 Pull Request 后,项目经理和其他团队成员开始查看并审查他的代码。经过几轮讨论和修改,最终小李的优化功能被成功合并到主仓库。
通过这种工作流,小李意识到,Fork 和 Pull Request 提供了一种高效的协作模式,开发者可以在独立的仓库中进行开发,不会影响主仓库的稳定性,同时也能保持代码的清晰和有序。
第十六章:Git 的持久化——“GitHub Actions” 帮我自动化部署
随着项目的需求变得越来越复杂,小李和团队开始考虑如何将代码部署过程自动化。为了减少人工操作,提高效率,小李提议使用 GitHub 的 CI/CD 功能——GitHub Actions。
“小王,我们可以用 GitHub Actions 来实现自动化部署吗?” 小李询问道。
“当然可以!GitHub Actions 是 GitHub 提供的 CI/CD 服务,能够在你每次推送代码时自动触发一系列动作,比如编译、测试、部署等。” 小王一边讲解,一边打开了 GitHub 仓库的设置页面。
小李兴奋地学习着如何配置 GitHub Actions。他创建了一个新的 .yml 配置文件,定义了自动化的流程。每次有新的代码推送到主分支时,GitHub Actions 会自动执行一系列操作,首先进行单元测试,然后编译代码,最后自动将更新部署到生产环境。
小李的配置文件如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
name: CI/CD Pipeline
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: "14"
- name: Install dependencies
run: npm install
- name: Run tests
run: npm test
- name: Deploy to production
run: npm run deploy
这个配置文件定义了当代码推送到 main 分支时,GitHub Actions 会自动进行代码的检查、依赖安装、测试和部署过程。小李将这个文件推送到 GitHub 仓库中后,团队中的每个成员都能在代码推送后享受到自动化部署的便利。
不久之后,当小李提交代码并推送到 GitHub 上时,他看到 GitHub Actions 自动触发了部署流程,并成功将代码部署到生产环境。每次提交后,他不再需要手动执行部署操作,GitHub Actions 自动为他完成了这一切。
“真是太棒了!这下我们可以更专注于开发,不用再浪费时间在部署上了。” 小李高兴地对小王说。
小王点头道:“是的,自动化部署让我们可以更快速、更高效地推送代码,也减少了人为错误。”
第十七章:Git Submodule——为项目添加依赖库
随着项目的扩展,团队决定将一些通用的库或模块独立出来,作为子模块来进行管理。这些子模块将在多个项目中复用,减少了重复代码的编写。
“小李,我需要你帮助把我们当前的公共库添加为 Git 子模块,这样其他项目也可以引用这个库。” 项目经理安排了一个新任务。
“好的,git submodule 是怎么操作的?” 小李询问。
“git submodule 是 Git 提供的一个工具,它允许你将一个 Git 仓库嵌套在另一个 Git 仓库中。你可以在主项目中添加其他的 Git 仓库作为子模块,通过子模块来管理依赖。” 项目经理解释道。
小李按照项目经理的要求,输入了以下命令,将公共库添加为子模块:
1
git submodule add https://github.com/example/public-library.git libs/public-library
通过这个命令,Git 将公共库克隆到主项目中的 libs/public-library 目录,并将其添加为子模块。小李继续执行了:
1
git submodule update --init --recursive
这样,子模块的所有内容都成功同步到本地。当其他团队成员需要使用这个公共库时,他们只需要在主仓库中执行相同的 git submodule 命令来同步子模块。
“小李,感谢你的帮助!以后其他项目也能通过这个子模块共享公共库了,极大减少了重复开发的时间。” 项目经理赞扬道。
小李点点头,意识到 Git 的 submodule 功能极大地提升了团队的开发效率,让依赖库的管理变得更简单、更灵活。
第十八章:Git 的高效工具——“Git Cherry-pick”
小李在团队开发的过程中,逐渐接触到了更多的 Git 高级操作。随着项目逐渐向前推进,他发现有时需要从一个分支中挑选特定的提交,而不是直接合并整个分支。比如,有时他只想把某个特定的功能或者修复应用到当前工作中,而不希望将整个分支的其他修改都合并过来。
“小王,有没有什么方法能让我只选取某个提交而不是合并整个分支?”小李问道。
“当然有,”小王笑着说道,“Git 有一个非常有用的命令,叫做 git cherry-pick,它可以让你从另一个分支上挑选特定的提交,并将该提交应用到当前分支。”
小李顿时豁然开朗:“那如果我想把某个功能从 feature 分支中拿到当前的 main 分支上,该怎么操作?”
“你只需要找到那个提交的哈希值,然后用 git cherry-pick 命令把它应用到你的当前分支。”小王答道。
于是,小李开始操作:
首先,他通过 git log 查找 feature 分支中的那个提交的哈希值。
1
git log feature
然后,他切换到 main 分支,准备将提交引入当前分支:
1
git checkout main
接着,使用 git cherry-pick 来选择并应用那个提交:
1
git cherry-pick
当命令执行后,Git 自动将那个提交的更改应用到了 main 分支上,而不需要合并整个 feature 分支。小李检查了代码,确认应用成功后,顺利地将新功能整合到当前分支。
“小王,真是太棒了!这个命令太实用了,以后再也不用为合并多余的代码而烦恼了!”小李高兴地说道。
小王点头笑道:“对,git cherry-pick 就是为了解决这个问题,它让你可以选择性地引入更改,避免不必要的合并。”
小李开始在日常开发中频繁使用 git cherry-pick,每当他需要从其他分支挑选特定提交时,这个命令都成为了他的得力助手。
第十九章:Git 与团队协作中的高效沟通
在一次代码审查的过程中,小李和团队成员遇到了一些小小的矛盾。由于对某个功能实现的理解不同,大家对如何修改代码意见不合。虽然问题本身并不大,但由于缺乏清晰的沟通,导致开发的进展有些停滞。
“小李,你能给我们解释一下你这次修改的思路吗?”项目经理耐心地询问。
小李站在桌前,深吸一口气,决定用更清晰的方式来表达自己的想法。他打开了 Git 提交记录,逐一展示了自己的修改和思路。
“我在这里用 git commit --amend 修改了之前的提交,因为在初始提交时,我没有充分考虑到性能问题。通过这次修改,我优化了这一部分。”小李一边说,一边展示了代码改动的细节。
项目经理点点头:“明白了,这样修改的确能够提升性能。但是,我们最好在团队中进行一些小范围的讨论,再决定如何优化。”
小李决定更加注重团队的沟通,他理解到 Git 提交的详细记录和清晰的提交信息可以极大地帮助团队成员理解每个开发者的修改意图。在随后的开发过程中,他在每次提交时,都更加注重编写简洁且易懂的提交信息。
“小李,你这次提交信息写得很好,大家都能清楚知道你的修改意图。”项目经理称赞道。
这次经验让小李意识到,Git 不仅是一个版本控制工具,它还成为了团队成员之间沟通的桥梁。通过清晰的提交记录和及时的 Pull Request,每个开发者都能了解其他人的工作,从而更好地进行协作。
第二十章:Git Tag 的应用——版本管理的好帮手
随着项目开发的深入,版本发布的节奏也逐渐加快。小李开始频繁接触到版本管理的任务。每当团队开发出一个新功能并完成测试时,他们就会创建一个版本发布,并用 Git 的 tag 来标记发布的版本。
“小李,接下来我们需要为即将发布的版本打上标签。” 项目经理走过来,指着屏幕说道,“你可以使用 git tag 来为我们当前的版本创建一个标签,并标记一个明确的版本号。”
“我明白了,标签就像是一个历史的快照,可以帮助我们标记每一个发布版本。” 小李回答道。
于是,小李在完成最后的代码修改后,为当前的提交打上了一个标签:
1
git tag v1.0
然后,他通过 git push 推送了标签到远程仓库:
1
git push origin v1.0
项目经理看到远程仓库成功更新了标签后,表示满意:“很好,tag 可以让我们轻松标记每次发布的版本,方便后续的维护和版本回溯。”
小李意识到,Git 的 tag 功能不仅能够帮助团队在版本发布时更加清晰地管理项目,还能在遇到回滚或版本回退时快速恢复到特定的版本。随着团队开发的推进,tag 成为了团队工作中不可缺少的工具之一。
第二十一章:Git 工作流中的“Branch”与“Merge”
随着小李逐渐掌握了 Git 的基础和高级操作,团队中的工作流程也开始更加成熟。在一次团队会议上,项目经理提议引入一种新的 Git 工作流,以便更高效地管理开发和部署过程。
“小李,团队正在采用 Git 的 feature branch 工作流,我们建议每个开发者在开发新功能时,都创建一个新的分支,开发完成后再合并回主分支。”项目经理说道。
“这样可以避免多人同时修改同一代码文件时产生冲突吗?”小李问道。
“没错,”项目经理解释道,“通过使用 feature branch,每个开发者都可以在独立的分支上进行开发,确保主分支保持稳定。只有在开发完成后,才能通过 git merge 或者 git pull request 合并回主分支。”
小李听后对这种工作流产生了浓厚兴趣。他立刻实践了这个流程。在开发新功能时,他从 main 分支创建了一个新的分支,并在分支上进行修改。当功能开发完成后,他执行了:
1
2
3
git checkout main
git pull origin main
git merge feature-new-feature
通过这种方式,小李能够确保每个功能都能在独立的环境中开发,不会影响其他团队成员的工作。而且,git merge 能够帮助他将所有更改平滑地合并到主分支,确保代码的稳定性。
Git,开发者的最佳伙伴**
随着时间的推移,小李不断地在实践中熟练掌握 Git 的各种技巧和工作流,他不仅能够高效地管理代码和版本,还能帮助团队提高开发效率。Git 成为了他工作中不可或缺的伙伴,见证了他从初学者到专业开发者的成长。
每当遇到问题或新的挑战时,小李总能依赖 Git 解决问题。他逐渐明白,Git 不仅仅是一个工具,它更像是一位教练,帮助开发者从错误中学习,从实践中进步。
未来的路还很长,而 Git 将继续陪伴小李,助力他在开发的世界中越走越远。在这个信息化飞速发展的时代,Git 让团队协作变得更加高效,代码管理变得更加清晰,开发者的每一步成长,都将在 Git 的世界中留下深深的印记。
第二十二章:Git 进阶——掌握 git reset 和 git reflog
随着开发项目的不断发展,小李逐渐遇到了一些复杂的情况。有时候,他在提交后发现代码存在问题,或者做了不必要的操作,想要撤回。对于这种情况,git reset 命令成为了他解决问题的利器。
一天,小李正在开发一个新功能,突然意识到自己在提交时选择了错误的文件进行修改,造成了不必要的代码变动。他想要撤回这次提交,怎么做呢?
“小王,我不小心提交了错误的代码,能不能撤回?” 小李焦急地问道。
小王走过来,微笑着解释道:“你可以使用 git reset 来撤回最近的提交。git reset 允许你恢复到某个特定的提交状态,你可以选择是保留本地修改,还是完全丢弃。”
“那如何使用呢?”小李问道。
“你可以通过 git reset --soft HEAD~1 撤销最近一次提交,但保留你的工作区更改;或者使用 git reset --hard HEAD~1 完全撤销提交,并且丢弃所有更改。” 小王继续说道。
小李决定使用 git reset --soft 来撤回错误的提交并保留工作目录中的修改:
1
git reset --soft HEAD~1
执行完命令后,Git 将最近一次提交撤销,并把修改恢复到暂存区。这时,小李可以继续修改代码并重新提交。
“小王,真是太方便了!git reset 让我能够灵活控制我的代码,避免了不必要的错误提交。”
“是的,git reset 是一个非常强大的命令,但要小心使用,尤其是 --hard 参数,使用不当可能会丢失数据。” 小王提醒道。
不仅如此,小李还学会了使用 git reflog 查看历史操作的记录。当他不小心做错操作时,git reflog 成为了解决问题的救命稻草。
“git reflog 是一个很有用的工具,它记录了你所有的 HEAD 操作历史,即便你执行了 git reset 或者其他改变了历史的操作,它也能帮你找回丢失的记录。” 小王向小李展示了 git reflog 的使用。
1
git reflog
小李通过 git reflog 快速找到了丢失的提交,并通过 git reset 恢复了之前的状态。这让他更加有信心在复杂的开发任务中使用 Git 来管理版本和操作历史。
第二十三章:Git 在代码审查中的优势
随着项目的逐渐深入,团队的代码审查变得尤为重要。小李不再是唯一负责提交和修改代码的人,团队中的每个成员都在贡献自己的力量。而 Git 在代码审查中的作用,也变得愈发突出。
“小李,今天我们来做代码审查,你的这段代码有一些地方需要调整。” 项目经理提醒道。
“好的,项目经理,能不能给我一些反馈?”小李走到项目经理的办公桌旁。
项目经理通过 Git 提交记录查看了小李的代码,指出了其中的一些问题。通过 git diff 命令,他能清楚地看到小李在提交时修改了哪些部分。
“小李,你在提交时改变了这个函数的实现逻辑。我们能不能保留原有逻辑并优化一下性能?”项目经理问道。
小李立刻打开终端,执行 git diff 查看具体的代码差异,确认了项目经理的建议:
1
git diff HEAD~1
“确实是这样。谢谢你指出这个问题,我会按照建议进行修改。”小李认真地说道。
通过 git diff,小李能够迅速查看每次提交所带来的代码差异,确保每次修改都能符合团队的要求。而且,Git 也让代码审查变得更加高效,团队成员可以轻松查看每个提交的改动内容,避免了沟通上的误解。
项目经理总结道:“Git 在团队协作中的优势非常明显,代码审查时,我们能直接通过提交记录看到每个开发者的修改,而且能随时回溯历史,查看每个修改的细节。”
小李深刻认识到,Git 提供的强大工具不仅能帮助他高效管理代码,还能在团队合作中提升代码审查的效率,确保每个成员的代码质量。
第二十四章:Git 和持续集成(CI)结合
随着项目的规模不断扩大,团队决定引入持续集成(CI)工具,以便更好地自动化测试和部署流程。小李负责配置 Git 和 CI 工具的集成,让团队的代码自动化构建、测试和部署。
“小李,我们要使用 Jenkins 来进行自动化构建和测试,能帮我把 Git 和 Jenkins 集成起来吗?”项目经理问道。
“没问题,项目经理,我来配置。” 小李点头答应。
首先,他在 GitHub 仓库中配置了 Webhook,当代码推送到 GitHub 仓库时,Webhook 会自动触发 Jenkins 构建任务。接着,小李在 Jenkins 中配置了一个构建任务,并在构建脚本中加入了自动拉取 Git 仓库代码的命令:
1
git clone https://github.com/your-username/project-repo.git
此外,Jenkins 还配置了自动化测试步骤,每当代码推送到 Git 仓库时,Jenkins 会自动拉取代码,执行单元测试,确保没有新的 bug 被引入。
通过这种方式,小李实现了 Git 与 Jenkins 的无缝集成,让每次代码推送后都能自动触发构建和测试流程,极大提高了开发效率。
“小李,你的配置太棒了!现在我们每次推送代码后,Jenkins 会自动进行构建和测试,这样就能第一时间发现问题,避免了手动操作的麻烦。”项目经理感慨道。
小李也深刻感受到,Git 不仅是团队协作的工具,还能与 CI 工具结合,提升整个开发流程的自动化和高效性。
第二十五章:Git 的深层次运用——多仓库管理
随着小李在项目中不断积累经验,他开始接触到更加复杂的 Git 使用场景。例如,团队中的多个子项目之间需要进行协调开发,这就需要管理多个仓库。为了有效管理这些子项目,小李学习了如何使用 Git 管理多个仓库的代码。
“小李,我们有多个子项目需要同步更新,能不能管理多个 Git 仓库?”项目经理问道。
小李思索了一下,答道:“我们可以使用 Git 的 submodule 功能来管理这些子项目,或者通过 git remote 将多个远程仓库关联到一个本地仓库。”
他通过 git remote 添加了其他项目的远程仓库,使得多个仓库能够通过一个 Git 仓库进行管理。这种方法让小李能够方便地同步多个子项目的代码,并确保所有仓库的代码始终保持一致。
“小李,这种管理方式非常有效,能让我们轻松地同步多个仓库的代码。”项目经理满意地说道。
第二十六章:Git 在应对大规模项目中的应用
随着团队逐渐发展,项目的规模也在不断扩大,甚至开始涉及多个跨团队的合作。小李渐渐意识到,在这样的大规模项目中,Git 不再是一个单纯的版本控制工具,而是整个开发流程的核心之一。为了解决不同团队成员之间的协作问题,团队决定采用 Git 作为核心工具来协调各项工作。
“小李,我们的项目越来越复杂,多个团队的协作需求也越来越高,如何更好地协调和管理多个模块之间的关系呢?” 项目经理问道。
“我建议我们将每个模块或子系统作为独立的 Git 仓库,采用 Git 的 Submodule 或者 Subtree 功能来管理不同模块之间的依赖。” 小李回答道,“这样可以让每个团队独立工作,同时通过 Git 的功能保持各个模块之间的同步。”
“Submodule 和 Subtree?能不能再详细说一下?” 项目经理有些疑惑。
“好的,” 小李微笑着解释,“git submodule 用于将一个 Git 仓库嵌套到另一个仓库中,适合在一个大的项目中引用一些公共的库或者子项目。它能够让我们独立管理每个模块,并通过主仓库来同步这些模块。git subtree 则是一个更强大的工具,允许我们将一个项目的子目录作为另一个仓库的历史部分来管理,方便合并和共享代码。”
项目经理点点头,表示理解:“那我们可以先尝试使用 git submodule,看看能不能有效管理各个子模块。”
于是,小李开始在主项目中引入了 Git 子模块,将各个子模块独立管理,每个团队都可以独立开发自己的部分,通过 Git Submodule 来同步。每当有更新时,团队成员只需使用 git submodule update 命令来同步各自的子模块。
“小李,这样的做法确实很有效。每个团队都可以专注于自己负责的模块,且能通过 Git 保证模块间的同步。” 项目经理满意地说道。
随着项目的不断发展,团队的协作也变得更加高效。Git Submodule 成为了管理大规模项目中多个模块之间关系的关键工具,使得每个团队能够更加高效地独立开发,避免了冲突和重复工作。
第二十七章:Git 和 DevOps 的融合
随着团队在多个项目中的逐渐积累,团队的开发模式也逐渐从传统的开发方式向 DevOps 转型。持续集成(CI)、持续交付(CD)和自动化测试成为了团队开发的核心需求,而 Git 作为版本控制工具在 DevOps 中的作用变得尤为重要。
“小李,接下来我们要将开发流程进行 DevOps 化,我们需要一个自动化的 CI/CD 流程来提高开发效率。” 项目经理指示道,“你能帮忙把 Git 与我们的 CI 工具结合起来吗?”
“没问题,我来配置。” 小李迅速答应道。
小李首先配置了 Git 与 Jenkins 的集成。每当团队成员向 Git 仓库推送代码时,Jenkins 会自动检测到提交,触发自动化构建流程,执行单元测试,确保每次提交的代码都能够通过测试。接着,他配置了持续交付流程,每当通过测试后,Jenkins 会将代码自动部署到开发环境进行进一步的验证。
为了进一步优化流程,小李还在 GitHub 上配置了 Webhooks,将每次 Git 提交推送事件通知给 Jenkins,确保流程的自动化和即时性。
“通过这种方式,我们的代码每次提交后都会自动构建和测试,减少了手动操作的时间和错误。” 小李解释道,“这种 DevOps 思维方式不仅提升了开发效率,也保证了代码的质量。”
项目经理看着自动化部署流程的顺利运行,表示满意:“非常好,Git 与 DevOps 结合,自动化构建和测试提高了我们的交付效率,减少了人工操作的错误。”
第二十八章:Git 在开源项目中的协作模式
随着小李在公司中积累的经验越来越丰富,他开始参加一些开源项目的开发。开源项目通常有很多开发者参与,其中涉及到不同的工作流和版本管理方式。在参与开源项目时,Git 的使用成为了开发中的一个重要部分,尤其是如何高效地与其他开发者协作。
“小李,最近我在 GitHub 上看到一个非常有意思的开源项目,我们也可以参与其中贡献代码。” 小王激动地说。
“开源项目?那我们要如何参与其中呢?”小李问道。
“我们可以通过 Fork 该项目,并在我们的个人 GitHub 仓库中进行开发。当我们完成自己的功能后,通过 Pull Request 向原项目提交我们的更改。” 小王解释道。
小李立刻开始了解开源项目的工作流程。首先,他在 GitHub 上 Fork 了项目,然后将其克隆到本地。接着,他在本地进行了修改,并在完成之后推送到个人仓库。
然后,他创建了一个 Pull Request,请求将自己的更改合并到原项目中。通过这种方式,原项目的维护者可以查看他的修改并决定是否接受他的更改。
“小李,Fork 和 Pull Request 真是开源项目的核心工作流。它能让我们独立开发,并通过 PR 与原项目进行贡献。”小王感慨道。
通过参与开源项目,小李不仅进一步提升了自己的开发技能,还学会了如何在全球范围内与其他开发者协作。Git 的强大功能使得开源项目的开发变得有序而高效,团队成员之间能够通过清晰的提交记录和 PR 进行有效沟通,确保了代码质量和开发进度。
第二十九章:Git 与 Agile 开发的结合
随着团队的进一步发展,团队决定采用敏捷开发(Agile)方法来提高开发效率和灵活性。敏捷开发要求团队快速响应需求变化,并在较短的时间内交付高质量的代码。而 Git 成为了实现这一目标的关键工具之一。
“小李,我们决定引入敏捷开发,采用迭代的方式进行功能开发。每个 Sprint 结束后,我们需要提交一个可交付的版本。” 项目经理说道,“你认为我们如何利用 Git 来支持敏捷开发?”
“我认为,Git 能够非常好地支持敏捷开发。” 小李答道,“通过创建 feature 分支,我们能够快速开发出独立的功能。在每个 Sprint 结束时,我们可以通过 git merge 或者 git pull request 将功能合并到主分支,从而保证代码的稳定性。”
“那我们如何处理版本发布呢?” 项目经理问道。
“Git 的 tag 功能非常适合版本管理。在每次功能开发完成后,我们可以通过 git tag 打上版本标签,标记每个发布的里程碑。” 小李解释道。
通过使用 Git 支持敏捷开发,团队能够更快速地进行功能开发和迭代交付,同时保持代码的清晰和可维护性。每次迭代结束后,Git 的分支管理和版本标签都确保了每个功能都能够按时交付,且不会影响其他功能的开发。
项目经理表示赞赏:“Git 的分支管理和 tag 功能完美支持了我们敏捷开发的需求,让我们能够更加高效地进行迭代和发布。”
Git —— 开发者的长久伙伴**
随着时间的推移,小李不仅在公司中积累了丰富的 Git 使用经验,也在开源项目和敏捷开发中不断提升自己。Git 成为了他日常开发工作中不可或缺的工具,帮助他在团队中与其他成员高效协作,推动项目顺利进行。
Git 不再只是一个简单的版本控制工具,它已经深入到整个开发流程中,成为小李的长久伙伴。无论是日常的功能开发,还是复杂的多团队协作,Git 都能够帮助开发者高效管理代码,提升开发效率。
未来的道路依然充满挑战,但小李相信,只要有 Git 作为工具,他将能够不断突破自己,迎接更大的成功。在这个快速发展的技术时代,Git 将继续陪伴小李,在他编程的旅程中不断迈向新的高度。
第三十章:Git 在大规模团队中的管理实践
随着公司项目的规模日益壮大,团队成员也越来越多。小李和其他团队成员发现,Git 在大规模团队中的协作变得更加复杂。在一个多团队、跨部门的项目中,如何有效地管理和合并不同的代码变得尤为重要。
“小李,团队的规模扩大后,我们发现管理多个开发分支变得越来越困难,很多人都在不同的分支上并行开发,合并冲突也变得更频繁了。你觉得我们怎么做才能更高效地管理代码?” 项目经理问道。
小李开始思考,发现随着团队规模的扩大,代码管理面临的挑战确实在增加。他想到了一些改进方法:“我们可以采用 Git Flow 工作流来更好地管理分支。Git Flow 是一个标准化的分支管理模式,它通过规定各个分支的功能来减少冲突,确保每个分支的职责明确。”
“Git Flow?听起来很有用。你能详细解释一下吗?” 项目经理有些好奇。
小李耐心地讲解道:“Git Flow 是由 Vincent Driessen 提出的分支模型,它定义了几个主要的分支角色:
master:主分支,保存着所有已经发布过的代码,始终保持稳定。
develop:开发分支,所有新的功能都在这个分支上开发,只有当功能开发完成并经过测试后,才会合并到 master 分支。
feature:特性分支,用来开发独立的功能模块。每个功能都会在一个单独的 feature 分支上开发,开发完成后合并回 develop 分支。
release:发布分支,主要用于准备新版本的发布。当 develop 分支上的功能基本完成时,我们会创建一个 release 分支来进行最终测试和 bug 修复。
hotfix:修复分支,快速修复主分支上的紧急 bug,修复完成后合并到 master 和 develop。
这种工作流的优势在于,每个团队成员可以在自己的特性分支上独立开发,减少冲突。而且,release 和 hotfix 分支帮助我们在发布和修复过程中保持主分支的稳定。”
项目经理听后点点头:“这个工作流确实很有意义,它能够帮助我们更有条理地管理代码,避免过多的冲突和合并问题。”
小李随后帮助团队将 Git Flow 工作流应用到团队项目中,确保每个开发人员按照规范创建分支、提交代码,并使用 git merge 或 git pull request 合并分支。通过这种方法,团队成员之间的协作变得更加高效,而 Git Flow 也为团队提供了清晰的分支管理结构。
第三十一章:Git 在多平台开发中的角色
随着团队的技术栈不断扩展,项目开始涉及多个平台的开发需求:前端使用 React,后端使用 Node.js 和 Express,移动端使用 React Native,甚至还有一些用于数据处理的 Python 脚本。每个平台的代码和环境不同,但如何确保这些平台的代码都能同步更新、管理起来却不产生冲突,成为了小李的一个新挑战。
“小李,考虑到我们开发的多个平台,如何更好地管理这些不同代码的依赖呢?” 项目经理询问道。
小李回忆起自己在过去的项目中遇到过类似的需求,顿时有了主意:“我们可以利用 Git 子模块(git submodule)来管理跨平台的代码库。通过将每个平台的代码作为独立的 Git 仓库,使用主仓库来统一管理这些代码,就能有效地将不同的代码库集中在一个地方,方便我们管理。”
“你是说,不同的代码库可以独立管理,然后通过主仓库来同步吗?” 项目经理问。
“是的,git submodule 允许我们将其他 Git 仓库嵌套在主仓库中,作为子模块来管理。每个子模块有自己的版本号,主仓库负责对子模块进行版本控制和更新。当我们需要更新子模块的代码时,只需在主仓库中同步子模块即可。” 小李解释道。
他接着为项目配置了 Git 子模块,每个平台的代码库都作为一个子模块被添加进主仓库中,使用 git submodule 命令来同步更新各平台的代码:
1
2
3
git submodule add https://github.com/example/frontend-repo.git frontend
git submodule add https://github.com/example/backend-repo.git backend
git submodule add https://github.com/example/mobile-repo.git mobile
每当更新子模块的代码时,团队成员只需要在主仓库中执行:
1
git submodule update --recursive --remote
通过这种方式,团队能够有效管理不同平台的代码,并确保每个平台的版本都能保持同步,减少了版本冲突的发生。
“通过子模块管理多个平台的代码,团队成员可以独立开发,不同平台之间不会相互干扰。” 项目经理表示赞赏。
第三十二章:Git 与 Code Review 流程
随着项目的不断发展,代码质量和团队合作变得尤为重要。小李意识到,除了版本控制和代码同步,如何高效地进行代码审查(Code Review)也是团队合作中的一个关键环节。
“小李,最近我们在团队中遇到了一些代码质量问题。为了提高代码质量,我们决定加强代码审查流程。” 项目经理说。
“我明白,代码审查能帮助团队发现问题并保持一致的编码风格。我觉得 Git 可以在这方面发挥很大作用。” 小李答道。
小李向项目经理提议,通过 Git 提交记录和 GitHub 的 Pull Request 功能,团队成员可以高效地进行代码审查。每次提交新的功能或修改时,开发者都通过 git push 将代码推送到远程仓库,并创建一个 Pull Request,请求团队成员进行审查。
在代码审查过程中,团队成员可以在 GitHub 上查看具体的改动(git diff),留下评论,提出修改意见。每次修改完毕,开发者会更新 Pull Request,直到所有问题得到解决。最终,项目经理或负责人会合并代码并批准最终的提交。
“小李,这个流程非常有帮助,Git 的 Pull Request 功能能让我们更高效地进行代码审查,确保每个功能都经过了充分的讨论和审核。” 项目经理说道。
通过 Git 提供的强大功能,团队能够在代码审查中更好地协作,不仅提升了代码质量,还加强了团队之间的沟通。
第三十三章:Git 在自动化和脚本化中的应用
随着团队项目越来越复杂,小李开始接触到一个新的需求——自动化。项目中有很多重复性、繁琐的操作,比如同步子模块、打标签、发布新版本等。小李发现,手动执行这些操作既耗时又容易出错,因此他决定通过编写脚本来自动化这些任务。
“小李,我们希望能简化每次发布的过程,能不能帮忙写个自动化脚本,避免每次都要手动执行 Git 命令?” 项目经理提出了一个需求。
小李非常兴奋,因为他一直对自动化非常感兴趣。他思考了一下,决定用 Bash 脚本来实现这一功能。脚本的目标是每次新版本发布时,自动更新子模块、切换到主分支、打标签,并将代码推送到远程仓库。
他编写了如下脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
# 1. 更新所有子模块
git submodule update --init --recursive
# 2. 切换到主分支
git checkout main
# 3. 拉取最新的代码
git pull origin main
# 4. 打上新的版本标签
VERSION=$(date +"%Y%m%d%H%M%S")
git tag -a "v$VERSION" -m "Release version $VERSION"
# 5. 推送主分支和标签到远程
git push origin main
git push origin "v$VERSION"
echo "Release $VERSION successfully created and pushed!"
这个脚本首先更新了所有子模块,确保每个模块都是最新的。然后,它会切换到 main 分支,拉取远程仓库的最新代码,接着生成一个基于当前时间戳的版本号,并使用 git tag 打上标签。最后,脚本会将 main 分支和新标签都推送到远程仓库。
“小李,这个自动化脚本简直太棒了!以后发布新版本时,直接执行这个脚本就行了,避免了手动操作的错误。” 项目经理大加赞赏。
小李也对这个自动化过程非常满意。通过自动化脚本,他不仅减少了繁琐的操作,还降低了人为错误的风险。这次经历让小李更加深刻地认识到,Git 不仅是版本控制工具,它也能够与自动化流程紧密结合,提升整个开发流程的效率。
第三十四章:Git 与团队沟通的桥梁
随着团队规模和项目复杂度的增加,沟通变得尤为重要。团队成员之间不仅要讨论功能需求,还需要清楚地了解其他成员的工作进展。小李发现,Git 提供的提交记录、分支管理、合并过程和 Pull Request,实际上都为团队沟通提供了强有力的支持。
一天,小李在查看项目的 Git 提交记录时,注意到某些提交信息比较模糊,无法准确反映开发的进度和内容。他认为,这是团队在使用 Git 时未能充分利用其沟通能力。
“小王,你觉得 Git 提交信息的重要性有多大?”小李向同事请教。
“其实提交信息是团队沟通的一个重要桥梁,它可以帮助我们了解每个功能的开发进度和思路。如果提交信息不清晰,团队成员很难理解开发者的意图。” 小王认真回答道。
“那我们能不能有一个规范,确保每个人在提交时都写清楚相关信息?”小李提议。
小王点点头:“是的,我们可以制定一些提交信息规范,比如每次提交时都写清楚这次提交的目的、所修复的 bug、改进的功能等。这样可以帮助团队成员快速理解代码的变动,提升沟通效率。”
于是,小李和团队一起讨论并制定了一份 Git 提交信息规范,要求每次提交时都必须包括以下内容:
简短的标题:描述这次提交的主要目的(如“修复登录功能”、“优化数据处理逻辑”)。
详细的描述:具体说明这次提交修改了什么,为什么要修改,以及如何修改的。
关联的 issue 或 bug:如果是修复 bug 或实现某个需求,提交信息中需要提及关联的 issue 或 bug 编号。
从那以后,团队的 Git 提交记录变得更加清晰和规范,每个人在查看提交记录时都能迅速理解变更内容。Git 不再只是一个简单的工具,它成为了团队沟通的桥梁,帮助开发者更好地协作。
第三十五章:Git 与远程协作的无缝结合
在小李所在的公司,随着项目的扩展,团队成员开始分布在不同的地方,甚至有了远程工作模式。如何保持远程团队的协作效率,成了一个关键问题。小李深知,Git 的强大远程协作能力将在这里发挥巨大的作用。
“小李,考虑到我们团队已经是远程工作,我们需要有一个标准化的流程来管理远程仓库和分支,确保每个人都能高效协作。”项目经理提出了新的挑战。
“我们可以利用 GitHub、GitLab 或 Bitbucket 等平台的功能来实现这一点,配合 git fetch、git pull、git push,保证每个人的工作始终与远程仓库同步。” 小李回答道,“我们可以设置清晰的分支管理规则,例如每个功能使用独立的 feature 分支,合并时使用 Pull Request,并且严格要求审查代码。”
“那这样的话,团队成员都可以在自己的分支上独立工作,最后通过 Pull Request 来进行合并和代码审查,对吗?” 项目经理进一步确认。
“是的,完全正确。每个人都可以在自己的分支上工作,提交并推送到远程仓库后,其他成员可以随时拉取更新。通过 Pull Request 和代码审查,我们可以确保代码质量,避免直接推送到 main 分支。” 小李解释道。
为了更好地管理远程协作,小李还建议团队使用 GitHub 的团队管理功能,将每个项目的成员分成不同的权限组,确保项目代码的安全性。
通过这些措施,团队的远程协作变得越来越顺畅,开发进度也大大加快。Git 成为他们跨时区、跨地域协作的桥梁,保证了远程工作模式下的高效沟通与协作。
第三十六章:Git 与知识管理的结合
在一次团队会议上,项目经理提到:“我们团队已经积累了大量的知识和技术文档,如何能够更好地管理这些知识,让每个新加入的成员都能快速了解项目和技术栈?”
小李听后立即想到了 Git 的优势:“我们可以将这些文档和知识管理工作与 Git 结合起来,创建一个专门的 Git 仓库,来管理项目的技术文档、架构设计以及解决方案等。”
项目经理表示认可:“这样一来,新成员可以通过 Git 仓库查看我们的文档,也能通过 Git 提交的历史记录了解每个问题的解决过程。”
小李和团队决定创建一个专门的 Git 仓库,专门用来存储项目文档、设计文档、代码规范和技术文章等。在这个仓库中,所有文档都将与代码一样进行版本控制,每个文档的更新和修改都会记录在 Git 提交历史中,方便后续查看和追溯。
通过这种方式,团队的知识管理变得更加高效,每个成员都可以随时查看、更新和修改文档,而 Git 则确保了文档的版本控制和更新记录。
第三十七章:Git 在复杂环境中的应用——跨平台开发与持续集成
随着项目变得越来越复杂,小李发现开发环境的多样化带来了新的挑战。在一个典型的跨平台开发环境中,团队同时在开发前端、后端、移动应用、甚至机器学习模型等多个部分,而每个部分都有不同的技术栈和依赖。
“小李,我们的项目现在涉及多个平台,前端是 React,后端是 Node.js,移动端是 React Native,机器学习模型是用 Python 实现的。如何在这些平台之间保持一致,并且高效协作呢?” 项目经理问。
“这个问题确实不小,”小李思考了一下,“不过我想到了一种方式。我们可以利用 Git 和 Docker 来帮助管理这些平台的环境,确保每个开发者的环境一致。同时,我们可以通过 CI 工具进行自动化测试和构建,确保每个平台的代码都能顺利运行。”
他继续讲解:“首先,我们可以为每个平台创建独立的 Git 子模块,分别管理每个平台的代码库。然后,我们可以通过 Docker 为每个平台创建独立的开发环境,确保每个开发者都能在本地轻松搭建相同的开发环境。”
项目经理表示理解并表示认可:“这个思路不错,Git 的子模块能帮助我们管理各个项目,而 Docker 则能保证每个开发者的环境一致。接下来,你可以为我们实现这一方案吗?”
小李开始实施方案。他为每个平台创建了独立的 Git 子模块,将不同平台的代码库分开管理,确保每个平台的代码能独立开发且相互不干扰。接着,他为每个平台编写了 Dockerfile 文件,确保每个开发者都能通过 Docker 快速搭建一致的开发环境。
例如,前端的开发环境通过 Docker 配置了 Node.js 和相关的构建工具,而后端则使用了 Express 和数据库连接,机器学习的环境则配置了 Python 和 TensorFlow。通过 Docker 容器,开发者只需要拉取相应的容器镜像,就可以快速搭建起完全一致的开发环境。
“小李,这个方案非常棒!通过 Docker,我们能够在不同平台之间确保开发环境的一致性,Git 子模块则让我们能更好地管理不同平台的代码。” 项目经理感慨道。
小李也对这一方案非常满意。通过 Git 和 Docker 的结合,团队能够有效地管理和协作多个平台的开发,避免了环境不一致的问题。同时,自动化测试和持续集成的引入也让开发进度更加高效。
第三十八章:Git 在数据库版本控制中的重要性
随着项目的功能越来越丰富,数据库的管理也变得尤为重要。数据库结构、存储过程和数据迁移等方面的变动需要进行详细的记录和管理。小李发现,传统的数据库管理方式往往不够透明,难以追溯数据库的变动历史。于是,他提出了一个新方案:使用 Git 来管理数据库的版本控制。
“小李,我看到你最近在研究数据库版本控制的方案。你能给我们讲讲怎么将 Git 应用到数据库管理中吗?” 项目经理问。
“我想到了一个方法,就是通过将数据库的结构和迁移脚本也放入 Git 仓库中,和应用代码一起管理。” 小李回答道。
他接着解释:“我们可以把每次数据库的变动(例如表结构变更、索引创建、存储过程更新等)写成迁移脚本,并将这些脚本放入 Git 仓库中进行版本控制。每当需要修改数据库结构时,开发者只需编写迁移脚本,并将其提交到 Git 仓库。然后,通过自动化工具或者手动方式运行这些迁移脚本,数据库就能与应用代码同步更新。”
项目经理觉得这个方案非常有价值,毕竟数据库的变更往往难以管理,Git 的引入能够帮助团队更好地追踪和管理这些变动。
“小李,你可以为我们建立一个数据库迁移管理流程吗?” 项目经理问道。
小李开始构建这个方案。他创建了一个新的 Git 仓库,用于存储所有数据库的迁移脚本。每次数据库结构有变化时,开发者会编写相应的迁移脚本,提交到 Git 仓库中。迁移脚本的命名规则也被统一,以便于后续的管理和回滚。
当需要更新数据库时,开发者会使用一个数据库迁移工具(例如 Flyway 或 Liquibase)来自动应用这些脚本,确保数据库与应用代码保持一致。如果数据库出现问题,可以通过 Git 历史记录查看之前的变更,并回滚到某个历史版本。
“小李,你的这个方案太好了!数据库变更再也不需要手动记录和更新了,所有的变动都可以通过 Git 来追溯。” 项目经理激动地说道。
通过将数据库版本控制与 Git 集成,团队不仅提高了管理效率,还增加了开发过程中的透明度和可追溯性。
第三十九章:Git 与团队文化的融合
随着项目逐渐走向成熟,小李逐渐意识到,Git 不仅仅是一个技术工具,它也在潜移默化地影响着团队的文化。在团队的协作中,Git 强调了透明度、代码的可追溯性和团队成员之间的高效沟通。
“小李,最近我注意到,团队成员在协作时越来越注重代码的清晰度和规范性,大家的合作也越来越顺畅。” 项目经理感慨道,“你觉得这与 Git 的使用有关系吗?”
小李点点头:“有很大关系。Git 强调了每个人对代码的责任和透明度。每次提交都可以清晰地查看修改内容,团队成员通过 git diff 和 git log 能够清楚地看到彼此的工作。这不仅提升了代码质量,也促进了团队成员之间的有效沟通。”
项目经理继续说:“我还发现,团队成员越来越注重提交信息的规范,大家在写提交信息时,会写得很清晰,标明修复的 bug、优化的功能以及其他细节。这种良好的习惯促进了团队的协作和项目的高效推进。”
小李微笑着回答:“是的,Git 的提交信息规范性确实对团队合作有很大影响。当每个开发者都知道自己提交的每一行代码都需要经过审查并且能被追溯时,大家会更自觉地保证代码的质量和清晰度。Git 让我们在开发过程中养成了良好的编码习惯,也提升了团队协作的效率。”
项目经理点头赞同:“Git 作为我们的核心工具,不仅帮助我们管理版本、协作开发,还帮助我们树立了良好的团队文化。我们现在的开发过程更加高效,团队也更加和谐。”
第四十章:Git 与 DevSecOps 的结合
随着公司项目和团队规模的扩大,小李逐渐意识到,除了版本控制和协作,Git 在保障代码安全性方面也能发挥重要作用。在项目中,团队开始引入 DevSecOps(开发、运维和安全一体化)的概念,确保代码在整个开发周期中都能遵循最佳的安全实践。
“小李,我们最近决定加强项目的安全性,将安全控制纳入到开发流程中,你觉得 Git 在这个过程中能扮演什么角色?” 项目经理问道。
小李思索了一下,回答道:“Git 在 DevSecOps 中的作用主要体现在两个方面。一方面,它能够帮助我们确保代码的安全性,通过代码审查和历史提交记录,让我们清晰地了解代码变动,确保没有恶意或不安全的代码被引入;另一方面,Git 还能够与安全工具集成,通过 CI/CD 管道自动化地检测漏洞,保证每次代码提交都经过安全审查。”
“能举个例子吗?” 项目经理感兴趣地问道。
小李接着解释道:“举个例子,我们可以在 Git 提交时,集成一些静态代码分析工具,例如 SonarQube 或 Checkmarx,这些工具会扫描每次提交的代码,检测潜在的漏洞或安全隐患。如果发现问题,Git 会阻止代码的合并,直到问题被修复。通过这种方式,我们能够确保每个提交都符合安全标准。”
项目经理点头称赞:“这个思路很好,Git 不仅能管理版本,还能与安全工具紧密结合,自动化检查代码中的安全问题。这将大大提高我们的安全性,避免漏洞进入生产环境。”
小李开始为团队配置了与 Git 集成的安全工具。他设置了 SonarQube,确保每次代码提交时,都会自动进行静态代码分析,并通过 Git 的 hook 阻止潜在的漏洞进入主分支。通过这种方式,团队确保每次提交的代码都能符合安全标准,而不需要人工介入。
第四十一章:Git 在微服务架构中的管理
随着项目架构的复杂性增加,团队决定将项目的开发模式转向微服务架构。微服务架构将应用拆分成多个独立的服务,每个服务都负责一个特定的功能模块,能够独立部署、独立开发,并且通过 API 进行互联。
“小李,随着我们逐渐采用微服务架构,如何高效地管理每个微服务的代码和依赖呢?” 项目经理问道。
“我们可以使用 Git 来管理每个微服务的代码库,确保每个微服务都能独立开发和部署。” 小李回答道,“每个微服务都可以作为一个独立的 Git 仓库来管理,这样能够保证服务之间的独立性,并且可以通过 Git 的 Submodule 或者 Subtree 来管理微服务之间的依赖。”
“小李,那微服务间如何进行版本控制和依赖管理呢?” 项目经理继续问。
“我们可以在每个微服务的 Git 仓库中使用标签(tag)来标记每个版本,确保我们可以回溯历史版本。” 小李进一步解释道,“此外,通过在 Git 仓库中配置 CI/CD 流程,每个微服务都能独立构建、测试和部署,确保每个服务的更新不会影响其他服务的运行。”
项目经理表示赞同:“这样的话,每个微服务就能独立管理,同时保持与其他服务的同步,减少了系统的耦合性。”
小李帮助团队将 Git 应用于微服务架构的管理中,为每个微服务创建了独立的 Git 仓库,并通过 Git 子模块来管理服务之间的依赖。通过这种方式,每个微服务的代码都能独立更新和发布,而 Git 确保了服务之间的同步和版本控制。
第四十二章:Git 与团队协作工具的集成
随着团队逐渐壮大,协作工具的使用也变得越来越重要。小李发现,除了 Git,本地的代码管理,团队还需要借助 Slack、Jira、Trello 等工具进行任务管理、沟通和协作。为了提高工作效率,如何将 Git 与这些工具集成,成为了小李的新挑战。
“小李,我们希望在每次代码提交时,能够自动通知团队成员并更新任务进度,你觉得 Git 能和这些工具集成吗?” 项目经理问道。
“完全可以,” 小李答道,“我们可以利用 Git 提供的 webhook 功能,将 Git 与 Slack、Jira 等工具集成。当我们提交代码时,可以自动通过 webhook 发送消息到 Slack,通知团队成员代码的更新;同时,我们也可以自动更新 Jira 上的任务状态,让团队成员更清晰地知道当前任务的进度。”
项目经理听后非常赞同:“这能大大提高我们的沟通效率,让每个团队成员都能第一时间知道任务进展。”
小李立即开始为团队配置 Git 与 Slack 和 Jira 的集成。他设置了 GitHub 的 webhook,在每次提交时,自动发送消息到团队的 Slack 频道,告知大家新的功能开发进度或 bug 修复的状态。同时,他也将 Jira 与 GitHub 集成,每次提交代码时,相关的任务会自动更新状态,确保任务进展清晰可见。
“小李,太棒了!通过这种集成,我们的沟通效率大大提高,团队成员能够第一时间看到任务的进展和代码更新。” 项目经理感慨道。
第四十三章:Git 的未来——AI 与自动化的结合
随着技术的不断进步,小李看到 Git 的发展趋势将不仅仅停留在传统的版本控制和协作管理上。人工智能(AI)和自动化工具的结合,将使 Git 变得更加智能化,能够自动分析代码质量,提供代码优化建议,甚至自动修复常见的代码问题。
“小李,你认为 Git 在未来的发展会是怎样的?” 项目经理问道。
“我认为,未来 Git 会与 AI 更紧密地结合,自动化和智能化的特性将进一步增强。” 小李回答道,“例如,Git 将能够通过 AI 自动分析代码的质量、风格和结构,自动生成优化建议,并通过机器学习模型来预测代码的潜在问题。未来的 Git 可能不仅仅是一个代码管理工具,它也可以作为开发中的智能助手,帮助开发者更高效地编写和管理代码。”
项目经理笑了笑:“这个思路非常前瞻,的确,AI 与 Git 的结合将为开发者带来更多的便利。也许在不久的将来,Git 就不再是一个仅仅执行命令的工具,而是一个智能的合作伙伴,主动为开发者提供帮助。”
小李对未来的 Git 充满期待。他相信,随着 AI 和自动化技术的发展,Git 将继续进化,成为开发者不可或缺的智能伙伴,不仅帮助管理版本和代码,更会成为整个开发过程中的决策支持系统。
第四十四章:Git 与版本控制系统的进化
随着技术的不断发展和项目需求的不断增加,小李渐渐意识到,Git 的功能已远远超出了传统的版本控制工具的范畴。越来越多的团队开始探索如何利用 Git 在更高层次的工作流和管理中发挥作用。小李对 Git 的深度掌握也逐渐使他开始考虑,未来是否有可能会出现新的版本控制系统,或者 Git 会如何进化以适应未来的需求。
“小李,你觉得 Git 在未来会如何发展?是否会有其他版本控制工具取代 Git 的地位?” 项目经理在一次会议中向小李提出了这个问题。
小李思索了一下,回答道:“我认为 Git 目前仍然是最强大的版本控制系统,它已经深深扎根于开发者的工作流中。不过,随着大数据、分布式计算和人工智能的快速发展,Git 可能会在未来进行一些技术性进化,特别是在性能优化、协作和自动化方面。可能我们会看到更多与 Git 集成的工具,或是智能化的 Git 提示系统,甚至 Git 会在管理大规模分布式系统中变得更加高效。”
他继续说道:“目前的 Git 是为开发者和小型团队设计的,但随着大型开源项目和企业级开发的需求增加,Git 在性能和复杂工作流方面仍然存在一定的局限性。未来可能会出现一种更加适应大规模并行开发、全球分布式协作和高度自动化的版本控制系统。”
项目经理点头表示认可:“你说得对,随着开发需求的多样化,Git 也必须不断进化,才能保持其在开发中的核心地位。”
小李也意识到,尽管 Git 强大且灵活,但未来开发模式的变化无疑将推动版本控制系统的发展。基于人工智能和机器学习的自动化工具,可能会为 Git 提供更加智能的版本分析和优化建议,同时帮助开发者快速发现潜在问题,甚至自动修复常见的代码错误。未来的 Git,可能不仅仅是一个代码管理工具,它可能变成一个主动帮助开发者的智能平台。
第四十五章:Git 与云原生技术的结合
随着 Kubernetes、Docker 等云原生技术的普及,团队的工作流程和开发环境也发生了巨大的变化。尤其是在微服务架构、容器化和云部署方面,Git 被赋予了新的角色。小李看到了 Git 与云原生技术结合的巨大潜力,开始思考如何将 Git 更好地融入到云原生的开发和部署流程中。
“小李,考虑到我们现在使用的 Kubernetes 和 Docker,如何将 Git 与这些工具结合,以便更好地支持我们在云环境中的开发和部署?” 项目经理提出了新的挑战。
小李微笑着回答:“Git 在云原生开发中的作用将变得越来越重要。它不仅可以用于代码管理和版本控制,还能与 CI/CD 工具和容器化管理平台如 Docker 和 Kubernetes 紧密集成。通过 Git,我们可以管理云原生应用的代码、配置和容器镜像的版本,并通过 Git 的工作流管理持续集成和交付。”
“能不能更具体地说说 Git 如何与这些工具集成?” 项目经理继续问。
“当然。我们可以通过 Git 来管理 Kubernetes 的 YAML 配置文件和 Dockerfile,每次提交都会自动更新这些文件并生成新的容器镜像。然后,我们可以将 Git 与 CI/CD 工具(如 Jenkins、GitLab CI 或 CircleCI)集成,让每次代码提交都触发自动化构建、测试和部署流程。当新的容器镜像构建完成后,Kubernetes 可以自动更新集群中的服务,确保所有服务运行的是最新版本的容器。” 小李解释道。
小李随后展示了如何将 Git、Docker 和 Kubernetes 融合在一起。他为团队配置了 GitLab CI,通过 GitLab 的 CI 管道,自动化地将每次提交的代码构建成 Docker 镜像,并将其推送到容器镜像仓库。然后,GitLab CI 会触发 Kubernetes 的滚动更新,将最新的容器镜像部署到集群中。
“这样一来,团队可以通过 Git 统一管理应用代码、容器镜像和配置文件,所有的操作都可以自动化完成,大大提升了开发和部署效率。” 小李补充道。
项目经理非常满意:“通过 Git、Docker 和 Kubernetes 的结合,我们能够实现更高效的开发和部署流程,这样我们的云原生应用管理会变得更加灵活和自动化。”
第四十六章:Git 与人工智能的融合——自动化代码审查
小李对 Git 的理解越来越深,他意识到,随着人工智能(AI)的发展,Git 与 AI 的结合将带来革命性的变化,尤其是在代码审查和质量保障方面。人工智能的智能化算法可以帮助自动化审查每次提交的代码,检测潜在的 bug、代码风格问题、甚至安全漏洞,从而进一步提高开发效率和代码质量。
“小李,你提到过 Git 可以与人工智能结合,我非常感兴趣。你认为 Git 和 AI 会如何结合呢?” 项目经理问道。
小李兴奋地回答:“未来 Git 可以与 AI 技术结合,自动化地分析每次提交的代码并提供反馈。比如,AI 可以通过机器学习算法分析代码的历史版本,识别出常见的代码问题、低效的代码结构和潜在的安全漏洞。每当开发者提交代码时,AI 会自动进行审查,甚至给出优化建议,帮助开发者提高代码质量。”
他继续解释:“AI 还可以根据项目的代码库和历史记录学习最佳实践,为每次提交提供个性化的建议。此外,AI 还可以通过分析大量的开源项目,帮助开发者学习其他项目的代码风格和技术实现,优化自己的代码。”
小李展示了如何将 Git 与 AI 集成,利用 GitHub Actions 和第三方 AI 插件,自动对每次提交的代码进行静态分析,检测潜在的 bug 和性能瓶颈。通过这种方式,开发者可以在代码提交之前获得实时的反馈,大大减少了错误的发生,并提高了代码的质量。
项目经理听后非常赞赏:“这个方案非常前沿,未来的 Git 不仅仅是一个版本控制工具,它还能成为智能的代码审查助手,主动帮助开发者提升代码质量。”
第四十七章:Git 与团队文化的深度融合
随着时间的推移,Git 不仅仅作为一个技术工具融入了团队的开发流程,也深刻影响了团队的文化。通过 Git,团队逐渐形成了一种透明、开放、协作的文化,所有的代码变更都能被追溯,团队成员之间的沟通变得更加顺畅。
“小李,最近我发现团队成员之间的沟通和协作变得更加高效了,大家在提交代码时,都在 commit message 中写得很清楚,代码审查时也能快速理解每个提交的目的和实现。” 项目经理总结道。
小李点头表示同意:“是的,Git 让我们的团队更透明。每个提交记录不仅仅是对代码的记录,也是对开发过程的记录。通过清晰的提交信息和 Pull Request,我们能够看到每个开发者的工作进展和思路,及时发现并解决问题。”
项目经理继续说道:“而且,Git 让我们保持了高效的协作,每个人都能在自己的分支上独立开发,只有经过审查和确认后,才会合并到主分支。这样我们不仅能保证代码质量,也避免了直接推送到主分支可能带来的风险。”
小李感慨道:“Git 让我们建立了一个高效、透明的开发文化,不仅提高了工作效率,还增强了团队之间的信任和合作。”
第四十八章:Git 与创新的结合——开源贡献与全球合作
随着团队项目的不断发展,小李的思维也逐渐扩展到全球开源社区的协作。在一次技术分享会上,项目经理提到了一个新的发展方向:“小李,我们是否可以将我们的部分技术栈或工具开源,贡献给全球开发者社区,获得更多的反馈和贡献?”
小李顿时眼前一亮:“开源是一个很好的方式,可以通过全球社区的力量推动项目进步。同时,Git 本身就是开源的,已经深深融入了全球开发者的工作流。我们可以通过 GitHub 或 GitLab 等平台将我们的项目开源,让更多的人参与进来。”
“你说得对,开源不仅能促进技术创新,还能让我们获得来自全球的反馈。” 项目经理笑道,“那我们开始考虑如何将我们的项目开源吧。”
小李开始探索如何将团队开发的工具和库开源,并通过 Git 管理开源项目。首先,他为项目创建了一个 GitHub 仓库,并在仓库中提供了详细的文档、安装指南、功能说明等内容。接着,他将项目代码整理好,确保它能与其他开发者无缝集成。
在开源项目的管理过程中,小李使用 Git 的 Pull Request 工作流来管理外部贡献。当其他开发者提出修改或优化时,团队通过审查 Pull Request 来评估贡献质量,最终合并到主仓库中。这种流程帮助团队高效管理外部的贡献,同时确保代码质量和稳定性。
“小李,看到这些来自全球开发者的贡献,我们不仅提高了工具的质量,也扩大了团队的影响力。” 项目经理感叹道,“通过 Git 和开源,我们实现了全球范围的技术合作。”
小李也感到无比兴奋,他知道开源不仅是对技术的贡献,更是一种全球化的合作方式。通过 Git 管理的开源项目,他与全球开发者共同推动了技术的进步。
第四十九章:Git 与 DevOps 2.0——智能化运维与持续交付
在一次团队会议中,随着 DevOps 实践的深入,小李意识到,随着自动化、人工智能和机器学习的引入,DevOps 也逐渐进入了智能化的新阶段。团队在原有的 CI/CD 管道上进一步优化,加入了智能化运维和持续交付(Continuous Delivery,CD)的理念。
“小李,现在我们已经将 CI/CD 集成到项目中,但如何进一步优化并提高运维的智能化水平呢?” 项目经理问道。
小李思索了一下,回答道:“我认为,我们可以通过将 AI 和机器学习引入到 DevOps 流程中,提升运维和发布的智能化水平。Git 可以与这些新技术结合,通过持续监控、数据分析和预测算法,帮助团队提前发现潜在的问题,甚至自动执行修复。”
他继续解释:“例如,我们可以通过 Git 与机器学习模型的结合,分析每次提交的代码,预测可能的性能瓶颈或代码错误。根据这些预测,CI/CD 管道可以提前生成优化建议,甚至自动修复代码中的常见问题。”
项目经理对这个思路非常感兴趣:“这个方向非常有前景。Git 不仅仅是管理版本的工具,未来它还可能成为我们 DevOps 2.0 中智能化运维的一部分。”
小李开始着手实现这一方案。他集成了 Git、Jenkins 和 AI 模型,通过分析每次提交的代码,预测潜在的性能问题和安全漏洞,并将预测结果通过 Git 提交记录反馈给开发者。当 Git 检测到潜在问题时,自动发出警告,并触发自动化修复任务。
“通过 Git 与 AI 的结合,我们的 DevOps 2.0 管道变得更加智能化,能够自动监控、分析和修复代码中的问题,减少了人工干预。” 小李兴奋地说道。
项目经理也非常满意:“这不仅能提高开发效率,还能让我们更早地发现并解决问题,减少了运维的负担。”
第五十章:Git 与自动化测试的无缝对接
随着团队对自动化的需求增加,团队决定将自动化测试与 Git 完全集成,确保每次提交和部署都能通过自动化测试来保障代码质量。在过去的项目中,虽然有一些自动化测试,但由于没有与 Git 紧密结合,很多时候测试结果无法及时反馈给开发者,导致修复过程缓慢。
“小李,我们希望能够将自动化测试与 Git 完美集成,实现每次提交后自动执行单元测试,并将测试结果即时反馈给开发者。” 项目经理提出了新的需求。
“这个方案完全可以实现。” 小李回答道,“通过 Git 与 CI 工具的结合,我们可以在每次代码提交后自动触发测试脚本,确保代码的质量不受影响。同时,我们还可以将测试结果实时显示在 GitHub 或 GitLab 上,开发者可以第一时间看到自己的提交是否通过了测试。”
于是,小李开始配置 Git 与 Jenkins 和 Selenium 的集成。每次团队成员向 Git 提交代码时,Jenkins 会自动拉取代码并执行自动化测试,确保每个功能模块都经过严格的测试。测试结果会通过邮件或 Git 的通知功能反馈给开发者,确保开发者能及时修复问题。
“小李,这样一来,我们就能确保每次提交的代码都经过了自动化测试,不仅提高了代码质量,还能加快开发进程。” 项目经理满意地说道。
小李也意识到,Git 与自动化测试的结合将使开发流程更加高效和安全,减少了因人为错误或疏忽导致的问题。
第五十一章:Git 与云平台的深度集成
随着公司逐渐迁移到云平台进行开发和部署,云平台成为了小李和团队工作流程中的重要部分。如何将 Git 与云平台无缝结合,成为了小李在这一阶段的新挑战。
“小李,我们已经在使用 AWS 和 Azure 进行开发和部署,但如何将 Git 与云平台更好地结合,形成一个高效的开发和部署流程?” 项目经理问道。
“我们可以通过 Git 与云平台的 DevOps 工具集成,来实现自动化构建、部署和监控。” 小李回答道,“例如,通过将 Git 与 AWS CodePipeline 或 Azure DevOps 集成,每次代码提交后,Git 会自动触发云平台的构建和部署流程。我们还可以通过云平台的监控工具实时获取应用的运行状态,确保代码始终在稳定的环境中运行。”
小李帮助团队配置了 AWS CodePipeline,并通过 GitHub 集成了自动构建和部署流程。每当团队成员提交代码时,AWS CodePipeline 会自动拉取代码、构建应用并部署到 AWS 上进行运行。通过云平台的监控工具,团队可以实时查看应用的健康状态,确保应用的稳定性。
“通过 Git 与云平台的集成,我们能够将开发和部署完全自动化,不仅减少了人工操作,还能实时监控应用的状态。” 小李总结道。
项目经理对这个集成方案表示高度满意:“这大大提高了我们团队的开发和部署效率,也让我们在云平台上的管理更加便捷。”
第五十二章::Git——开发者永不止步的伙伴
回顾这些年,小李的 Git 旅程不仅仅是一个技术学习的过程,它已经深刻地影响了团队的工作方式,推动了团队从传统开发模式走向 DevOps、AI 和云原生的未来。Git 不仅仅是一个版本控制工具,它已经演化为开发流程的核心,融入了团队的文化和协作方式。
“小李,你的努力让我们的团队在技术上不断创新,从自动化测试到云平台集成,Git 在我们的开发流程中扮演了越来越重要的角色。” 项目经理感慨道。
“Git 是我职业生涯中最重要的伙伴,它让我从一个新手开发者成长为今天的技术专家。未来我会继续学习,继续进步。” 小李自信地回答。
无论技术如何进化,Git 都将继续成为开发者的重要工具,帮助他们管理代码、提升协作效率、自动化工作流程。随着时代的变化,Git 也将继续为开发者提供强大的支持,成为他们永不止步的伙伴。
---
## 懒猫微服开发篇(七): 解析 Docker Compose Override
- URL: https://blog.no-claw.com/posts/7ac076b/
- 日期: 2025-07-17
- 分类: 懒猫微服 / 开发
- 标签: Docker, 懒猫微服
看过很多的 Docker 教程,也都不曾提到过 compose override,第一次接触到这个是在懒猫微服上解开 LPK 看到的,用来注入 docker 引擎的环境变量。但是还以为是懒猫微服的小技巧,今天整理笔记才发现原来的 Docker compose 用来做多环境部署的配置文件,比如用来给开发和生产分别注入不同的环境变量和配置文件。
参考文档:Docker Compose Override - LazyCat Developer Guide
使用场景是这样,在实际开发中,通常我们需要分别为开发和生产环境配置不同的服务和环境变量。虽然可以为每个环境维护独立的 Compose 文件,Docker Compose 提供了一个非常有用的特性,可以将多个 Compose 文件结合使用,简化配置管理。
基础配置文件:第一个 Compose 文件通常作为基础配置,后续的文件可以覆盖该基础文件中的配置。
覆盖配置:每个额外的文件不仅可以覆盖基础文件中的已有配置,还可以添加新的配置。
默认情况下,Compose 会读取以下两个文件:
docker-compose.yml:基础配置文件
docker-compose.override.yml:覆盖文件
你还可以通过 -f 参数指定多个非默认的覆盖文件,Compose 会按顺序合并这些文件。
1
docker compose -f docker-compose.yml -f dev.override.yml up
docker-compose config:这是一个非常有用的命令,可以帮助你验证最终的配置文件,尤其是在使用多个 Compose 文件时。它显示了合并后的 Compose 配置,帮助你确保配置符合预期。
示例:Nginx 配置
docker-compose.yml
1
2
3
4
5
services:
web:
image: nginx:latest
ports:
- "80:80"
docker-compose.override.yml
1
2
3
4
5
6
services:
web:
volumes:
- ./dev:/usr/share/nginx/html # 使用本地开发文件夹覆盖默认卷
environment:
- DEBUG=true # 启用开发环境的调试模式
在这个例子中,docker-compose.override.yml 覆盖了 docker-compose.yml 中的配置,添加了开发环境相关的文件挂载和环境变量设置。
合并后的配置(查看通过 docker-compose config 命令生成的配置)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
name: "3"
services:
web:
environment:
DEBUG: "true"
image: nginx:latest
networks:
default: null
ports:
- mode: ingress
target: 80
published: "80"
protocol: tcp
volumes:
- type: bind
source: /Users/name/Desktop/dev
target: /usr/share/nginx/html
bind:
create_host_path: true
networks:
default:
name: 3_default
可以看到,通过合并配置,开发环境的调试模式和本地文件夹挂载已经成功加入了配置。
懒猫应用的上的 compose override
针对一些 lpk 规范目前无法覆盖到的运行权限需求, 可以通过 compose override 机制来间接实现。
通过应用查看器可以看到,这是 Docker Compose 配置的一部分,用于定义容器中的 containly 服务,并映射 playground 的 docker 引擎。具体的配置说明如下:
services: 这是 Docker Compose 文件的顶级字段,定义了服务列表。containly 是定义的一个服务名称。
containly: 这是服务的名称。在此配置下,的服务名是 containly。
volumes: 定义了容器与宿主机(本地)之间的文件夹共享和挂载。该部分的配置是映射一个本地目录到容器内部的目录。
bind: 使用绑定挂载的方式(bind mount),允许宿主机的文件或目录直接映射到容器内部。这里设置了 create_host_path: true,意思是如果宿主机上的 /data/playground 目录不存在,它会自动创建。
source: 宿主机的路径,映射为容器中的目录。这里指定了 /data/playground 作为源路径,意味着宿主机上的这个目录将被挂载到容器内。
target: 容器内的路径,即宿主机上的 source 目录映射到容器内部的 /lzcapp/run/playground 目录。容器内的应用可以访问这个目录。
type: 这里设置的是 bind,表示采用绑定挂载方式
最后 highlight 下 WIKI 里的三句话:
确认最终生成的 lpk 中存在名为 compose.override.yml 的文件,并且内容是一个合法的 Compose 合并文件。
通过 SSH 进入 /data/system/pkgm/run/$appid 目录,确认该目录下是否存在 override.yml 文件。
使用 lzc-docker-compose config 命令查看最终合并后的配置,确保它符合预期。
子/data/system/pkgm/run/$appid 目录里,我的结果如下,供参考。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
docker compose config
name: xudeploycontainly
services:
app:
....
- type: bind
source: /data/playground
target: /lzcapp/run/playground
bind:
create_host_path: true
...
(base) lzcbox-029c588e /data/system/pkgm/run/xu.deploy.containly # ls
compose.override.yml docker-compose.yml pkg
(base) lzcbox-029c588e /data/system/pkgm/run/xu.deploy.containly # cat compose.override.yml
services:
containly:
volumes:
- bind:
create_host_path: true
source: /data/playground
target: /lzcapp/run/playground
type: bind
---
## 懒猫微服进阶心得(十一):把懒猫微服当作旁路由,客户端无需转发直接访问内外地址
- URL: https://blog.no-claw.com/posts/5018903b/
- 日期: 2025-07-17
- 分类: 懒猫微服 / 进阶
- 标签: 懒猫微服, 代理
今天在外面通过懒猫转发访问路由器时,发现需要逐一映射多个内网地址,操作起来有点麻烦,于是想到了一种一劳永逸的解决办法。
最开始我用 Squid 来代理 AWS VPC,后来想到懒猫也可以用作同样的功能。以下是我准备的 Compose 配置,我已经完成了 app 镜像的复制。
1
2
3
4
5
6
7
services:
squid:
container_name: squid
ports:
- 3128:3128
image: registry.lazycat.cloud/u04123229/ubuntu/squid:c534e32e8e1e766e
networks: {}
下面是 Docker 截图:
默认安装后,Squid 的默认规则会屏蔽所有网站。若要访问内网地址,就会看到“访问被拒绝”的提示。接下来,我们需要修改转发规则。
进入 Squid 容器,编辑配置文件,然后重启容器即可生效:
1
2
3
sudo vim /etc/squid/squid.conf
# 在文件底部添加以下行以允许所有 HTTP 访问
http_access allow all
现在就可以使用你喜欢的工具进行访问了。
然后就可以在外边访问私有地址了。
或者在终端上,通过设置环境变量以通过代理服务器访问 HTTP 和 HTTPS:
1
2
export http_proxy=http://your_proxy_ip:3128
export https_proxy=http://your_proxy_ip:3128
如果你需要白名单功能:
首先,创建一个包含你希望允许访问的域名的白名单文件。该文件会用于匹配允许的域名。
打开终端并创建白名单文件。例如,我们将其存放在 /etc/squid/whitelist 路径:
1
sudo vim /etc/squid/whitelist
在文件中,每行列出一个希望允许访问的域名。例如:
1
2
3
.youtube.com
.bilibili.com
.example.com
这样就允许 youtube.com、bilibili.com 和 example.com 的请求通过代理,而其他未列出的域名将被拒绝。
2. 编辑 squid.conf 配置文件
接下来,编辑 Squid 的主配置文件 squid.conf,将白名单配置添加到文件中。
打开 Squid 配置文件:
1
sudo vim /etc/squid/squid.conf
在文件中,找到并添加以下规则:
允许 localhost 访问:
1
http_access allow localhost
该规则允许本地计算机(localhost)访问代理服务器。
添加白名单配置:
1
2
acl whitelist dstdomain "/etc/squid/whitelist"
http_access allow whitelist
这两行配置定义了一个名为 whitelist 的 ACL(访问控制列表),它从 /etc/squid/whitelist 文件中加载允许的域名。然后,我们允许匹配这些域名的请求。
拒绝所有其他访问:
1
http_access deny all
该规则确保只有白名单中的域名可以访问代理,其他所有请求都将被拒绝。
配置端口和缓存:
你可以在 squid.conf 文件中设置 Squid 的监听端口并配置缓存目录(如有需要)。例如:
1
2
http_port 3128
coredump_dir /var/spool/squid
---
## 懒猫微服进阶心得(十三): WebVirtCloud 安装 Windows 使用 virtIO 的注意事项
- URL: https://blog.no-claw.com/posts/147991a4/
- 日期: 2025-07-17
- 分类: 懒猫微服 / 进阶
- 标签: Docker, 懒猫微服
在使用 WebVirtCloud 部署 Windows 虚拟机时,如果想要获得更高的磁盘与网络性能,建议使用 virtIO 半虚拟化驱动。不过,Windows 并不像大部分 Linux 发行版那样自带 virtIO 驱动,因此安装过程中需要额外设置。本文结合社区已有教程与实测经验,整理了安装 Windows 时使用 virtIO 的注意事项,方便大家快速上手。
社区原教程参考:
半虚拟化与全虚拟化的区别
半虚拟化(Paravirtualization)和全虚拟化(Full Virtualization)的主要区别在于 虚拟机是否知道自己“不是物理机” 以及 是否需要为虚拟化修改驱动或操作系统。
特性
全虚拟化(Full Virtualization)
半虚拟化(Paravirtualization)
客户机是否知道自己在虚拟环境
否,操作系统认为自己在裸机上运行
是,操作系统知道自己在虚拟机里
是否需要修改客户机操作系统
否(原始 OS 可直接运行)
是(需要支持 paravirt 接口的内核或驱动)
是否模拟完整硬件
是,完全模拟 CPU、BIOS、设备
否,使用简化接口与 hypervisor 通信
性能
一般略低(依赖 VT-x/AMD-V 等硬件加速)
更高(减少陷入与上下文切换)
示例
VMware Workstation、QEMU + TCG、VirtualBox
Xen PV 模式、KVM virtio、Hyper-V Enlightenments
是否支持热迁移
支持
支持
全虚拟化:通过软件模拟硬件,客户机无需修改即可运行,但性能相对较低,尤其是 I/O。
半虚拟化:使用专用接口与宿主机通信,需要驱动支持,性能更高。
进一步对比如下:
情境
全虚拟化
半虚拟化(virtio-net)
客户机看到什么
模拟 Intel e1000 网卡
简化的 virtio-net 网卡
通信方式
模拟 PCI 总线、MMIO、DMA
共享内存 + 通知机制(virtqueue)
性能
中等(高 CPU 占用)
高(低延迟、低 CPU 占用)
兼容性
高(任何支持 e1000 的 OS 都能用)
需要安装 virtIO 驱动
简而言之:
全虚拟化 = 模拟“骗操作系统”
半虚拟化 = 协作“告诉操作系统你在虚拟机里”
安装 Windows 使用 virtIO 的步骤
准备 ISO 镜像
Windows 安装 ISO
virtIO 驱动 ISO(建议版本 virtio-win-0.1.266-1.iso)
下载地址:https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/virtio-win-0.1.266-1/
相当于安装的时候需要从 virtio 提取驱动文件来完成半虚拟化安装。
在 WebVirtCloud 设置中同时挂载两个 ISO
Windows ISO
virtIO ISO
启动控制台进入安装界面
加载 virtIO 驱动
安装向导默认找不到磁盘(因使用 virtIO 控制器)
点击“加载驱动程序” → 选择 virtIO ISO 中 w10 目录下的驱动
刷新后即可识别磁盘
正常分区并开始安装
安装过程,是漫长的等待。
首次启动时跳过联网
因为 virtIO 网卡驱动未安装,联网步骤可选择“我没有 Internet”跳过
进入系统后安装 virtIO 网卡驱动
在系统内安装 guest tools
打开 virtIO ISO
双击安装 guest tools(包含磁盘、网卡等驱动)
总结
在 WebVirtCloud 下安装 Windows 时使用 virtIO,可以显著提升磁盘和网络性能,但前提是正确加载驱动并在系统中安装 guest tools。整个流程的关键点在于:
安装阶段提前挂载 virtIO ISO 并加载驱动
首次启动跳过联网,进入系统后安装 guest tools
Linux 系统通常无需额外驱动,但 Windows 必须手动安装
这样,你的 Windows 虚拟机不仅能正常运行,还能充分发挥 KVM 的 I/O 性能优势。
---
## 懒猫微服进阶心得(十二):不登陆机器,如何使用 Docker Context 玩转微服容器?
- URL: https://blog.no-claw.com/posts/8be8e345/
- 日期: 2025-07-17
- 分类: 懒猫微服 / 进阶
- 标签: Docker, 懒猫微服
平时管理远端的 Docker 容器,大多数人第一反应是通过 SSH 登录到服务器再执行命令。
其实 Docker 本身是 C/S 架构,只要配置好连接方式,就能在本地直接管理远程容器,甚至用 VS Code 图形化界面操作,完全不必反复登录。
下面我就用管理微服容器的例子,把实现方法和使用体验记录下来
1. 准备免密登录
为了避免奇怪的认证问题,可以先将 SSH Key 复制到远端,实现免密登录。
(/root 目录重启不会丢失 SSH Key)
1
ssh-copy-id root@name.heiyu.space
2. 最简单的方式:设置 DOCKER_HOST
直接在本地设置 Docker 引擎的环境变量,指向远端的 docker.sock 文件:
1
export DOCKER_HOST=ssh://root@name.heiyu.space
在 Warp 中(非直接 SSH 登录),执行 docker ps 后,就可以看到远端的容器了。
不过仔细看,这里其实是系统组件,没有必要随便动,而且千万不要随便操作系统组件容器!
3. 使用 Docker Context 管理非标准路径的 docker.sock
之前介绍过,playground 和 appstore 的 Docker 配置文件在其他目录。
这种情况下可以通过 docker context 引用非标准路径的 docker.sock:
1
2
3
4
docker context create my-remote-sock \
--docker "host=ssh://root@name.heiyu.space:/data/playground/docker.sock"
docker context use my-remote-sock
playground 的 Docker 也可以用 Dockge 管理,不过当 Dockge 的功能不够用时,就可以用这个作为备用方案。
4. 管理商店容器
商店有时需要执行 lzc-docker exec 或 lzc-docker restart,可以先创建对应的 Context:
1
2
docker context create lzc-remote-sock \
--docker "host=ssh://root@name.heiyu.space:/lzcsys/run/lzc-docker/docker.sock"
然后切换 Docker 引擎:
1
docker context use lzc-remote-sock
查看 Docker 信息:
1
2
3
4
5
6
7
8
9
Kernel Version: 6.5.0-0.deb12.4-amd64
Operating System: Debian GNU/Linux 12 (bookworm) (containerized)
OSType: linux
Architecture: x86_64
CPUs: 8
Total Memory: 31.12GiB
Name: lzcbox-029c588e
ID: 0726989e-2e3e-46d1-89ba-753f7dd1a600
Docker Root Dir: /lzcsys/run/data/system/docker
5. 本地 Warp 记录
到这里,你会发现本地和远端几乎没有区别。
6. 图形化管理:VS Code Docker 插件
在 VS Code 中安装 Microsoft 官方 Docker 插件,即可在界面中查看远端的 Image、Container、Logs,并且可以直接 exec 进入容器。
在 Docker Context 面板可以直接切换上下文(相当于 docker context use lzc-remote-sock),然后就能在 VS Code 中操作对应容器和镜像,无需手动敲命令。
7. 查看已创建的 Docker Context
在命令行查看所有 Context:
1
docker context ls
输出示例:
1
2
3
4
5
6
7
8
NAME DESCRIPTION DOCKER ENDPOINT
default * Current DOCKER_HOST based configuration ssh://root@name.heiyu.space
lzc-remote-sock ssh://root@name.heiyu.space:/lzcsys/run/lzc-docker/docker.sock
my-remote-sock ssh://root@name.heiyu.space:/data/playground/docker.sock
orbstack OrbStack unix:///Users/.orbstack/run/docker.sock
Warning: DOCKER_HOST environment variable overrides the active context.
To use a context, either set the global --context flag, or unset DOCKER_HOST environment variable.
总结
通过 docker context 配合 VS Code Docker 插件,不仅能用命令行直接操作远端微服容器,还能图形化查看容器状态、镜像和日志。
这种方式的好处是:
免 SSH 登录,管理更高效;
可切换多个 docker.sock,适合同时维护多个服务环境;
配合 VS Code,操作体验接近本地容器。
对于习惯 GUI 操作的人来说,这几乎就是远程 Docker 的“丝滑”管理方式。下次维护微服时,你也可以试试这一套。
---
## EC2 没有绑定 EIP,重启后 IP 会变?DDNS-GO 自动更新你的域名
- URL: https://blog.no-claw.com/posts/369cde6d/
- 日期: 2025-07-14
- 分类: 软件 / AWS
- 标签: AWS
在 IPV4 即将枯竭的年代,云服务商的 EIP 也开始收费了。对于没有弹性公网 IP(EIP)的云服务器,我们可以通过 DDNS-GO 实现动态域名解析。本文介绍了如何使用 Docker 快速部署 DDNS-GO,并借助 DDNS 实现动态域名绑定,从而让服务器即使公网 IP 变化,也能够实时更新域名解析记录,这样只需要使用域名访问,不再需要在控制台查看。
安装 Docker
使用官方安装脚本快速安装 Docker:
1
2
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
设置 Docker 开机自启并立即启动:
1
sudo systemctl enable --now docker
添加当前用户到 docker 用户组(避免每次用 sudo)
1
sudo usermod -aG docker $USER
生效方式:
推荐:重新登录终端会话
或使用临时方式立即生效:
1
newgrp docker
验证是否配置成功:
1
docker info
若无权限报错,则配置已生效。
Docker 中部署 DDNS-GO
我们将使用 Docker 的 host 网络模式挂载主机目录,确保 DDNS 能正常检测本地 IP:
1
docker run -d --name ddns-go --restart=always --net=host -v /opt/ddns-go:/root jeessy/ddns-go
/opt/ddns-go 是主机目录,你可以替换为任意路径,用于持久化配置。
启动后,DDNS-GO 的配置文件为 .ddns-go.yaml,位于挂载目录中。
初始化配置
部署完成后,打开浏览器访问:
1
http://:9876
你会看到 DDNS-GO 的初始化页面,如图所示:
DDNS-GO 是一个开源的动态域名更新工具,支持多个域名服务商,我的域名托管在 cloudflare 上,所以需要在 cloudflare 上申请一个 API-KEY 来做这个更新。
TTL 建议设置为“自动”
IP 获取方式推荐使用外网 API(如 ipip.net)
前往 Cloudflare 的 API Token 页面,为 DDNS-GO 创建一个具备修改 DNS 权限的 Token。
建议选择 “Edit zone DNS” 模板,只赋予必要权限,并可以限制在特定域名范围内使用。
此外,DDNS-GO 支持 webhook 通知,可选用如 Slack、Server 酱等方式实时通知 IP 变动情况。
验证 DDNS 的效果
我们尝试停止云主机后再重新开启,公网 IP 会发生变化:
重启后 DDNS-GO 会自动检测 IP 变动并更新域名解析:
通过域名访问服务仍然保持不变,无需手动更新 IP。
同时 Server 也会把这个消息推送到手机上:
总结
即使云服务器没有绑定弹性公网 IP,借助 DDNS-GO 和 Docker,我们依然可以实现动态域名解析:
低成本:无需购买 EIP,节省开销;
自动化:IP 改变后自动更新域名解析;
易部署:Docker 一键运行,配置简单直观。
---
## 懒猫微服实战入门(二十二):使用 DDNS-GO 给 Cloudflare 做动态域名解析
- URL: https://blog.no-claw.com/posts/6031f818/
- 日期: 2025-07-13
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服, 家庭网络
我的域名最初是在 AWS 的 Route53 上购买的,然而在使用过程中发现 Host Zone 的费用并不低,除了每月的托管费用,还包括 TTL 更新等开销。在朋友建议下,我决定将域名解析的任务从 AWS 迁移到 Cloudflare,一方面可以节省成本,另一方面配置也更为灵活。我家里的公网 IPV4 经常变动,所以我使用懒猫微服商店里的 DDNS-GO 和 Cloudflare 做动态域名解析。
第一步:更改 NS 记录
准确地说,需要在原域名注册商(我的是 AWS Route53)处修改 Name Server,将默认的 AWS NS 记录改为 Cloudflare 提供的 NS 地址。这样,域名解析权就转移到了 Cloudflare。Cloudflare 可以从懒猫微服的商店进入,也可以使用网页。而 DDNS-GO 使用商店快捷下载就非常方便。
Route53 中的 NS 修改界面 如下, 替换成 Cloudflare 的 NS 后,域名正式托管在 Cloudflare
第二步:创建 API Token
前往 Cloudflare 的 API Token 页面,为 DDNS-GO 创建一个具备修改 DNS 权限的 Token。
建议选择 “Edit zone DNS” 模板,只赋予必要权限,并可以限制在特定域名范围内使用。
第三步:配置 DDNS-GO
DDNS-GO 是一个开源的动态域名更新工具,支持多个域名服务商(不包括 AWS 的 Route53)。看来不仅迁移出来省钱,还省心。我们把上一步申请的 token 添加到这里,TTL 设置成自动就行。
TTL 建议设置为“自动”
IP 获取方式推荐使用外网 API(如 ipip.net)
此外,DDNS-GO 支持 webhook 通知,可选用如 Slack、Server 酱等方式实时通知 IP 变动情况。
第四步:查看效果
我将 DDNS 记录绑定到家用公网 IP(IPv4),未启用 IPv6。保存配置后,前往 Cloudflare 后台查看 DNS 记录,已成功同步更新。
⚠️ 小提示:如果你绑定的 IP 是中国大陆的,建议不要开启 Cloudflare 的代理功能(小云朵),否则可能出现连接问题。
Bonus:免费 TLS 证书
使用 Cloudflare 托管 DNS,访问你的网站时会根据规则加上 TLS 证书,实现 HTTPS 加密,非常方便省心。
小结
通过将域名解析迁移到 Cloudflare,并配合 DDNS-GO 工具进行动态更新,既省钱又省事,还顺带获得了免费证书加持。相比 AWS Route53 的托管费用和配置复杂度,Cloudflare 无疑是个人用户和轻量级应用的更优选择。
---
## 快速启动 http 站点
- URL: https://blog.no-claw.com/posts/572b7187/
- 日期: 2025-07-11
- 分类: 软件
- 标签: 开发
以下是 Python 内置 HTTP 服务器的几种常用启动方式:
默认端口启动(8000):
1
python -m http.server
指定端口启动(示例使用 1378 端口):
1
python -m http.server 1378
支持 IPv6 的启动方式:
1
python -m http.server 1378 --bind ::
这些命令会在当前目录启动一个简单的 HTTP 文件服务器,方便快速共享文件或测试网页。
---
## 用Filebeat OSS 7.10.2将收集日志到Easysearch
- URL: https://blog.no-claw.com/posts/a0786458/
- 日期: 2025-07-11
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
Filebeat OSS (Open Source Software) 7.10.2 版本是 Elastic 公司提供的开源版本。移除了一些Elasticsearch商业化的功能插件(但是没移出去干净)
https://www.elastic.co/cn/downloads/past-releases/filebeat-oss-7-10-2。
这主要还是 AWS 和 Elastic 公司之间的矛盾,AWS 托管的 ES 会和 Elastic 抢客户。所以 Elastic 搞了一个不能商业化的协议,影响了这个生态圈,也给开发者造成了很多不方便。
还记得这个非常戏剧性的 Issue:
https://github.com/elastic/beats/issues/8086
下面将介绍如何配置该版本 Filebeat 将日志发送到 Easysearch。
以下是完整的filebeat.yml配置示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/messages
- /var/log/httpd/*.log
output.elasticsearch:
hosts: ["yourEasysearchEndpoint:443"]
protocol: "https"
username: "username"
password: "password"
ssl.verification_mode: none
# 以下配置用于处理xpack相关兼容性问题
setup.template.name: "filebeat"
setup.template.pattern: "filebeat-*"
setup.template.enabled: true
setup.ilm.enabled: false
setup.template 和 setup.ilm 相关的操作就是和没移除干净的 xpack 有关系,所以得再配置文件加上这些东西。
同时也得在 Easysearch 里面开启兼容 ES 的 API,不然会遇到这个报错。
在我的 MBP 上是这样:(config/easysearch.yml)
1
2
3
discovery.type: single-node
network.host: 0.0.0.0
elasticsearch.api_compatibility: true
启动并测试 Filebeat:
1
2
sudo systemctl start filebeat
sudo filebeat test output
检查服务状态:
1
sudo systemctl status filebeat
生成测试日志:
1
echo "test log $(date)" | sudo tee -a /var/log/messages
在 Easysearch 中查询日志:
使用 Dev Tools 执行以下查询:
1
2
3
4
5
6
7
8
9
10
11
12
13
GET filebeat-*/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"@timestamp": {
"order": "desc"
}
}
]
}
成功响应示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
"took": 5,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": null,
"hits": [
{
"_index": "filebeat-7.10.2-2025.04.18",
"_id": "XXXXXXX",
"_score": null,
"_source": {
"@timestamp": "2025-04-29T09:04:09.566Z",
"log": {
"file": {
"path": "/var/log/messages"
},
"offset": 0
},
"message": "test log Fri Apr 29 09:04:00 UTC 2025",
"input": {
"type": "log"
},
"ecs": {
"version": "1.6.0"
}
}
}
]
}
}
曾经在 ubuntu 上遇到过只要启动 filebeat 就报错一堆内存的信息:
这个文章给了一个解决办法:
https://infinilabs.cn/blog/2025/ubuntu_run_filebeat/通过以上配置和步骤,您应该能够成功使用Filebeat OSS 7.10.2 版本将日志收集到 Easysearch 中。
---
## MacOS 环境下运行 EasySearch 报错无法信任 Java 包的解决方案
- URL: https://blog.no-claw.com/posts/b77119f9/
- 日期: 2025-07-11
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
问题背景
近期因 MacBook 系统降级重装,需重新部署 EasySearch 环境。由于系统未预装 Java,选择下载捆绑 JDK 的 EasySearch 版本,但在安装过程中遇到以下问题:
系统安全机制拦截 Java 运行
密码认证异常(持续返回 401 错误)
问题现象
安全拦截
MacOS Gatekeeper 阻止运行捆绑的 JDK,即使在「系统偏好设置-安全性与隐私」中手动放行后,仍出现权限不足提示(见图 1)。
认证失效
配置文件中的密码校验异常,任何登录尝试均返回 401 状态码。
解决步骤
全局权限设置
首先通过终端命令关闭系统安全限制:
1
sudo spctl --master-disable
但发现此操作仍无法解决 JDK 运行问题。
最终解决方案
采用处理「App 损坏」报错的方法:
重新解压安装包
执行扩展属性清除命令:
1
xattr -cr jdk/bin/java
重新初始化 EasySearch
验证结果
成功运行 EasySearch 并完成系统初始化(见图 3)。
经验总结
MacOS 对未公证应用的限制日趋严格,建议:
优先使用公证版本软件
遇到权限问题时,xattr -cr命令可有效清除可能导致拦截的扩展属性
401 错误可能与系统权限深度关联,需综合处理运行环境和配置文件
---
## 懒猫微服实战入门(二十一):使用 Memos 替代 Flomo
- URL: https://blog.no-claw.com/posts/6f97c31a/
- 日期: 2025-07-11
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服
曾经用过 Flomo,一度觉得体验不错,但后来因不想续费会员而中止使用。最近在懒猫微服的商店中发现了 Memos,完全开源、支持自托管,是个很不错的替代方案。
什么是 Memos?
Memos 是一款开源、极简、现代化的笔记系统,主打「快速记录、随时检索」。适合捕捉灵感、日常备忘、碎片想法、读书笔记等多种用途。
它具有以下特点:
🧩 极简 Markdown 编辑器,写作无干扰
🔍 支持标签与多条件过滤,查找便捷
📱 原生适配移动端界面,自动响应式布局
🌐 内置开放 API,可接入自动化工具或客户端
🗂 支持归档、置顶、图像插入、链接跳转等实用功能
🔐 数据完全自托管,掌控在自己手中
目前 GitHub star 数已超 6k,开发活跃,文档完善,社区生态也在不断壮大。
页面结构与使用方式
Memos 主页面分为两栏:
✅ 左侧侧边栏:
创建 Memo:点击加号即可撰写新内容
标签筛选:点击任意标签快速过滤
快捷菜单:查看所有 Memo、归档 Memo、置顶 Memo、搜索功能等
✅ 右侧内容区:
展示所有 Memo 内容,以时间倒序排列
每条 Memo 支持:
编辑、置顶、归档
标签添加(支持 #标签名 快捷方式)
Markdown 格式(支持标题、代码块、列表、引用等)
拖拽上传图片或截图粘贴
自动识别链接并可点击跳转
📷 示例页面如下:
开启 API 支持
在设置中可以启用 API 功能,系统会生成一个唯一的 API KEY,用于连接第三方客户端或自动化工具(如 Moe Memos)。
📷 API 开关和密钥生成页面如下:
Moe Memos 客户端
App Store 上的 Moe Memos 是一款第三方移动端客户端,原生适配 Memos 的 API 接口,界面极简,使用流畅。
📷 图标预览:
如何登录 Moe Memos
安装 Moe Memos 后,打开 App
输入你的自托管域名,如 https://memos.name.heiyu.space
填写 API KEY 即可登录(需在系统中开启单租户模式)
📷 登录后界面展示:
简单对比 Flomo
功能
Flomo
Memos(自托管)
微信同步
✅(原生)
❌(可接 webhook 实现)
多端支持
✅
✅ Moe Memos
数据隐私
❌ 云端托管
✅ 完全自持
开放 API
❌ 限制较多
✅ 支持 API KEY
成本
收费会员制
免费开源(除托管成本)
Markdown
❌
✅ 原生支持
图片粘贴上传
❌
✅
虽然 Memos 不支持微信原生同步,但凭借开放架构和高度可定制性,能实现更强的私有笔记体验。
总结
Memos 是一款功能恰到好处、设计极简、部署门槛低的笔记系统,非常适合替代 Flomo 这类灵感记录工具。配合懒猫微服提供的部署方案,可以做到:
一键上线,无需服务器运维知识
API 集成快捷,客户端体验佳
数据可备份、可迁移、可掌控
适合用来做「知识碎片记录」、「阅读随想」、「灵感管理」、「工程笔记」等场景。欢迎大家一起探索更多玩法!
---
## 懒猫微服实战入门(二十):如何使用 Time Machine 将 Mac 备份到懒猫微服
- URL: https://blog.no-claw.com/posts/c5298d72/
- 日期: 2025-07-10
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服, Apple
看了 Apple 的文档,时间机器备份到 SMB 或者外接设备(APFS)。正好懒猫微服带了 SMB 的共享。然后我们可以把时间机器备份到网盘里。虽然默认连接的是懒猫网盘根目录,但是我们也可以映射一个子文件夹 TimeMachine。如下:
一、 在 Mac 上连接到 SMB 共享
在 Finder 中,点击菜单栏的 “前往” (Go) > **“连接服务器…” (Connect to Server…)**。
在弹出的窗口中,输入 SMB 共享的地址,格式通常是 smb://[懒猫微服的IP地址或主机名]/[共享文件夹名称]。
例如:1
smb://192.168.1.100/用户名/timemachine
(域名也可以)
点击“连接”。系统会提示你输入用户名和密码。(懒猫微服的用户密码)
成功连接后,你会在 Finder 中看到这个网络共享。
二、 设置 Time Machine 进行备份
打开 **“系统设置” (System Settings)**(或旧版 macOS 中的“系统偏好设置”)。
点击 “通用” (General) > **“时间机器” (Time Machine)**。
点击 **“添加备份磁盘…” (Add Backup Disk…)**。
在弹出的列表中,你应该能看到你刚刚连接的 SMB 共享文件夹。
如果再次提示输入凭据,请再次输入你在懒猫微服上设置的用户名和密码。
Time Machine 会在共享中创建一个特殊的 .sparsebundle 文件,然后开始首次备份。
三、 解决 Time Machine 备份变慢的问题
在某些情况下,macOS 的默认设置或某些第三方应用程序可能会导致 Time Machine 备份变慢。
1. 暂时禁用磁盘节流(Disk Throttling)
macOS 默认会对后台进程(包括 Time Machine)进行磁盘 I/O 节流,以确保系统响应速度。在首次备份或需要快速完成备份时,可以暂时禁用此节流。
操作方法: 打开“终端”应用程序(位于“应用程序”>“实用工具”),输入以下命令并按回车键:
bash sudo sysctl debug.lowpri_throttle_enabled=0
输入管理员密码后,该设置会立即生效。请注意,这会使 Time Machine 占用更多系统资源,可能影响 Mac 在备份期间的流畅性。备份完成后,建议通过以下命令重新启用节流:
bash sudo sysctl debug.lowpri_throttle_enabled=1
2. 检查和禁用干扰备份的应用程序
某些应用程序,特别是防病毒软件、系统清理工具或文件同步工具,可能会频繁访问磁盘,从而干扰 Time Machine 的正常运行。
操作方法: 尝试在 Time Machine 备份期间暂时禁用这些应用程序,或者检查它们的设置,看是否可以将 Time Machine 备份盘从其扫描范围中排除。
3. 确保 Mac 保持唤醒状态
对于大型备份,如果 Mac 进入睡眠状态,可能会中断或减慢备份进程。
操作方法: 在备份期间,你可以前往“系统设置”>“显示器”(或“节能”)调整显示器关闭时间和电脑睡眠设置,或使用 caffeinate 命令让 Mac 保持唤醒:1
caffeinate -s -m -i -t 36000 &
此命令将使 Mac 在 10 小时内(36000 秒)保持唤醒,& 符号表示在后台运行。
四、 查看备份情况
可以按到我目前磁盘使用了 90 个 G,在时间机器中看到占用 50G。
在懒猫网盘中也可以看到这个数据。
通过以上步骤,你就可以成功地将你的 Mac 通过 Time Machine 备份到懒猫微服提供的 SMB 共享了。
---
## 给传统 NAS 玩家介绍一下懒猫微服
- URL: https://blog.no-claw.com/posts/47885d90/
- 日期: 2025-07-09
- 分类: 懒猫微服 / 番外
- 标签: 懒猫微服, NAS
玩过不少 NAS,从最早的黑群晖,威联通开始,后面陆陆续续接触了飞牛,绿联,极空间,UNRAID,EXSI,Proxmox VE,也算有点话语权。
首先最直观的区别是软件客户端,像群晖,威联通的电脑版客户端是用来发现机器的 IP 地址的,然后后续所有的操作都在浏览器完成,比如新建用户,登录,审计,访问数据,查看监控。
而懒猫微服的所有操作几乎都在客户端完成,网页端更像是一个应用的 Dashboard。然后访问的时候使用域名,配合厂家的穿透服务,无论你是互联网访问还是局域网访问机器,用这一个地址就够了,不再需要在路由器上做端口转发,也基本可以告别自己搭建内网穿透的痛苦了。
还有一个非常主要的是社群,懒猫微服既有官方的大群交流技术,也有 VIP 的小群私人定制。大群里有各式各样的玩家,除了懒猫微服之外大家还会交流各种 Github 的开源项目,Linux 技巧以及电脑外设心得, 然后每天大家都贡献攻略。因为公司 base 在武汉嘛,当然也会有接地气的武汉风情。 VIP 小群会针对个性化的需要做一对一的指导,比如曾经帮我排查了家庭宽带 DNS 污染,UPS 信号干扰,甚至 OpenID Connect (OIDC) 的使用和接入等问题,对于开源软件部署在懒猫上有问题也是尽全力 额度支持,届时可以拉一个小会,然后共享屏幕给他们查看。可以放心的 show linux 命令和飙专业术语啥的。 很多的问题可以在半小时到一个小时就能解决,有时候甚至会更短。
不只是自己写的软件部署上去访问有延迟他们会帮忙排查,而且一些商店里社区用户贡献的开源软件的问题,他们也会帮忙查看并且给出一些解答方式。 毕竟开源软件的 issue 真的不少,他们不会叫你去开 issue 然后漫长的等待,他们会去帮忙追。
而传统 NAS 的社群主要靠的是爱好者自己发起和维护的,有问题在群里问可能最后没有讨论出结果就不了了之了。甚至很多时候都会纠结到底是选 Proxmox VE 还是 EXSI。对于攻略部分,大多是散落在资深用户的个人博客上,比如很多威联通的文章都会发在什么值得买上面。而对于提问能不能得到答案,完全取决于作者用爱发电的程度了。如果商业 NAS 提案例,那么响应时间可能要很久,如果你提简单的案例,那么可能立马有人给你打电话,如果是比较难的问题,可能最后就不了了之了。只能说在服务客户这方面亚马逊是在是开了一个很不好的头,大概是这样:
因为 AWS 负责运行、管理和控制从主机操作系统和虚拟层到服务运营所在设施的物理安全性的组件。客户负责管理来宾操作系统(包括更新和安全补丁)、其他相关应用程序软件以及 AWS 提供的安全组防火墙的配置。客户应该仔细考虑自己选择的服务,因为他们的责任取决于所使用的服务、,这些服务与其 IT 环境的集成以及适用的法律法规。责任共担还为客户提供了部署需要的灵活性和控制力。
所以有时候遇到两个产品交叉的问题,我们一般是很难得到方案,很有可能两方都说这涉及三方产品你需要找对方,我们不了解你这个三方产品,没办法给你方案。而对于三方玩家几乎是没有办法同时把两个产品的售后叫到一起的。无论你是开案例还是拉一个会议,这难度很大很大。
然后是系统层面的对比,懒猫微服是 基于 debian 优化一套系统,后来又开了 root 权限,所以基本 Linux 能做的事他都能做,更是出厂预置了 dockage,dozzle 这样方面调试 docker 的软件,基本属于开箱即用。甚至我们还能使用 Docker 打包自己的软件然后上架给其他玩家使用,甚至可以对其他人说;
我的懒猫微服教程是中文圈里非常优秀的实战指南。
我的开发/移植软件也有非常多的懒猫用户在使用。
而开源的 NAS 基本上大家只玩虚拟机和按照教程配置 Docker,几乎是没有包管理工具的,所以安装的软件很受限。 如果真的想移植应用,那个开发者文档也是不太好看。起码对于普通玩家是真的劝退了。
最后一点也就是很重要的一点,关于 APP 访问这块。在从互联网访问这边,传统 NAS 有一个很大的痛点。一个是移动端软件适配不好,还有就是服务多了端口都得自己记住。再加上路由器转发的端口,真的很难记住了。而懒猫微服在移动端和 PC 上的页面几乎是差不多的,所有操作几乎可以在 APP 内部完成。应用商店上架的应用大部分都是 https 的协议和 443 的端口。不用自己做加密同时以及免去了记忆软件信息烦恼,毕竟即使是专业玩家,也不想天天做服务器的运维工作。
普通用户图一个方便。专业用户图一个折腾,然后缺少的就是一个稳定的穿透和传输,精力有限不想自己维护一些底层的东西。买个懒猫微服差不多全能解决了。
我一直有一个愿望,写一本 NAS 的书,让技术融入到爱好者的生活,技术不是枯燥的理论,当我们遇到问题的时候,才能想清楚他们为什么这么设计。技术人永不为奴。
---
## 懒猫微服实战入门(十九):我最喜欢的 RSS 阅读器-FreshRSS
- URL: https://blog.no-claw.com/posts/9fd148d9/
- 日期: 2025-07-09
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服
自建的博客都可以接入 RSS 订阅,我之很喜欢的一款 FreshRSS 在懒猫微服的商店上架了,可以联动一下了。
我的博客 RSS 订阅是:https://cloudsmithy.github.io/atom.xml
然后点击左上角的订阅管理,先新建一个分类,然后是输入 RSS 订阅地址即可。
我也添加了懒猫微服王总的博客订阅:https://manateelazycat.github.io/feed.xml
这个是我博客的订阅效果,这样在 PC 和移动端都可以访问了:
那如果想让用其他的软件订阅这个 FreshRSS 呢,在右上角点击设置,然后选择认证,点击允许 API 访问。然后就可以通过抓数据的方式进行访问。
然后回到账户管理,这个时候下面就出来了 API 管理这个选项,设置 token 然后就可以使用了。
然后我们访问 API 端点:https://freshrss.<机器名字>.heiyu.space/api/
这里可以看到 Google Reader compatible API 和 Fever compatible API。
1
2
3
4
5
6
7
8
9
10
11
FreshRSS API endpoints
Google Reader compatible API
Your API address:
https://freshrss.<机器名字>.heiyu.space/api/greader.php
Google Reader API configuration test:
✔️ PASS
Fever compatible API
Your API address:
https://freshrss.micro.<机器名字>.space/api/fever.php
Fever API configuration test:
✔️ PASS
那接下来使用 fluent-reader 订阅微服里的 FreshRSS。选择 Fever API,输入上边 RSS API 返回的 URL。
用户名和密码是登录 FreshRSS 的用户密码,然后端点是 Fever compatible API。(这里没有用到 token)
这个就是订阅的效果了。(FreshRSS 夹带私货自动订阅 release)
在 APP 里还挺好看的。(除了 fluent-reader 不支持检索)
用懒猫微服当作一个 RSS 存储的后端,同时也提供了一个 FreshRSS 的部署版本,很多记忆再也不会丢了。
---
## 懒猫微服开发篇(四):懒猫微服如何使用 OpenID Connect (OIDC)?(上)
- URL: https://blog.no-claw.com/posts/dcc47393/
- 日期: 2025-07-08
- 分类: 懒猫微服 / 开发
- 标签: 开发, 懒猫微服
OpenID Connect(OIDC)是一个基于 OAuth 2.0 的身份认证协议,允许用户使用一个账号(如 Google、微信、Microsoft 账号)登录多个不同的网站或应用,而无需重复注册。我们经常把他和OAuth 2.0 混为一谈。
它主要用于 身份认证(Authentication),而 OAuth 2.0 主要用于 授权(Authorization)。简单来说:
OAuth 2.0 → 让应用能访问你的数据(如获取微信头像),还要自己做用户管理。
OIDC → 让应用能确认“你是谁”(如用微信账号登录)
下面以我的懒猫 ENV 查看器为例,来讲解这个登录流程。
当你在应用处点击登录就会重定向到登录中心,我们通常管这个叫做身份提供商(IDP),如果是其他的软件有可能是 “使用 Google 登录” 或 “微信登录” 。
跳转到认证中心,一般都会提示你是否确认登录,某某应用将要获取登录的权限,查看你的信息。在懒猫微服里这直接点击 Grant Access 即可。在其他的 IDP 中,会让你输入账号密码登录,并同意授权该网站访问你的基本信息(如邮箱、昵称)。
当 IDP 验证完的身份后,返回一个 JWT(JSON Web Token),其中包含你的身份信息。当网站验证 JWT 后,确认你的身份,并让你登录成功。我们也可以在 jwt.io 和 jwt.ms 这个网站去做解码。
我解码了其中一个 token,我们可以看到里面的信息,可以看到加密算法,颁发机构,过期时间,用户信息什么的,
这个是一般登录的流程,比如首次用户名和密码登录成功之后会返回一个 JWT,然后后续把这个 JWT 当做 bear token 来请求后面的资源。我们的 OIDC 和这个原理类似,只不过稍微复杂一些。
在 OIDC 协议中,会遇到三种 Token: id_token, access_token 和 refresh_token。
Access Token 用于基于 Token 的认证模式,允许应用访问一个资源 API。用户认证授权成功后,Authing 会签发 Access Token 给应用。应用后面就带着这个** Access Token** 访问资源 API。
ID Token 相当于用户的身份凭证,开发者的前端访问后端接口时可以携带 ID Token,开发者服务器可以校验用户的 ID Token 以确定用户身份,验证通过后返回相关资源。
AccessToken 和 IdToken 都是 JWT,有效时间通常较短。通常用户在获取资源的时候需要携带 AccessToken,当 AccessToken 过期后,用户需要获取一个新的 AccessToken。
Refresh Token 用于获取新的 AccessToken。这样可以缩短 AccessToken 的过期时间保证安全,同时又不会因为频繁过期重新要求用户登录。用户在初次认证时,Refresh Token 会和 AccessToken、IdToken 一起返回。应用携带 Refresh Token 向 Token 端点发起请求时,这个时候会续签 AccessToken 和 IdToken 与 ID token。
所以我们一般说的 JWT 就是 Access Token 的部分用于授权。而ID Token 用户标注用户信息,Refresh Token 用来续签 Access Token 。
在懒猫微服上使用 OIDC 有一个好处就是,不用在 IDP 上填写申请信息,在程序运行过程中可以直接注入相应的环境变量,这样我们直接用就可以了。相当于传统 IDP 需要填写应用名称,做分组控制而言,这个自动注入的 OIDC 开箱即用很方便。
一般是有这几个信息:
CLIENT_ID:从我的 app 来看,这个就是包名
CLIENT_SECRET: 这个是随机的
ISSUER_URI:https://微服域名/sys/oauth
TOKEN_URI:https://微服域名/sys/oauth/token
USERINFO_URI:https://微服域名/sys/oauth/userinfo
先说 ISSUER_URI,这个是 OIDC 的入口,其中.well-known/openid-configuration 里可以拿到各种 URL,算是 OIDC 的入口,即使环境变量中没给信息我们也可以在这里查看。比如用来校验 JWT 的 jwks_uri。
GET https://<微服域名>/sys/oauth/.well-known/openid-configuration 结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
"issuer": "https://.heiyu.space/sys/oauth",
"authorization_endpoint": "https://.heiyu.space/sys/oauth/auth",
"token_endpoint": "https://.heiyu.space/sys/oauth/token",
"jwks_uri": "https://.heiyu.space/sys/oauth/keys",
"userinfo_endpoint": "https://.heiyu.space/sys/oauth/userinfo",
"device_authorization_endpoint": "https://.heiyu.space/sys/oauth/device/code",
"introspection_endpoint": "https://.heiyu.space/sys/oauth/token/introspect",
"grant_types_supported": [
"authorization_code",
"refresh_token",
"urn:ietf:params:oauth:grant-type:device_code",
"urn:ietf:params:oauth:grant-type:token-exchange"
],
"response_types_supported": ["code"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256"],
"code_challenge_methods_supported": ["S256", "plain"],
"scopes_supported": [
"openid",
"email",
"groups",
"profile",
"offline_access"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post"
],
"claims_supported": [
"iss",
"sub",
"aud",
"iat",
"exp",
"email",
"email_verified",
"locale",
"name",
"preferred_username",
"at_hash"
]
}
至于回调 URL,这个是需要自己设置的部分。可能由于开发习惯导致每个应用的回调 URL 不一样。相对于在 IDP 中填写信息,在懒猫微服的 lzc-manifest.yml 中加这么一行即可。也只有设置了 application.oidc_redirect_path 之后,才能使用 OIDC 相关的环境变量。
1
application.oidc_redirect_path: /callback
可以看看我的懒猫 ENV 查看器的设置。通过 oidc_redirect_path 设置回调地址,然后使用 environment 字段还这是需要的环境变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
lzc-sdk-version: 0.1
name: 懒猫ENV查看器
package: xu.deploy.env
version: 0.0.2
description:
license: https://choosealicense.com/licenses/mit/
homepage:
author: xu
application:
subdomain: env
oidc_redirect_path: /callback
routes:
- /=exec://5005,./lzcapp/pkg/content/run.sh
environment:
- LAZYCAT_AUTH_OIDC_CLIENT_ID=${LAZYCAT_AUTH_OIDC_CLIENT_ID}
- LAZYCAT_AUTH_OIDC_CLIENT_SECRET=${LAZYCAT_AUTH_OIDC_CLIENT_SECRET}
- LAZYCAT_AUTH_OIDC_AUTH_URI=${LAZYCAT_AUTH_OIDC_AUTH_URI}
- LAZYCAT_AUTH_OIDC_TOKEN_URI=${LAZYCAT_AUTH_OIDC_TOKEN_URI}
- LAZYCAT_AUTH_OIDC_USERINFO_URI=${LAZYCAT_AUTH_OIDC_USERINFO_URI}
- LAZYCAT_AUTH_OIDC_ISSUER_URI=${LAZYCAT_AUTH_OIDC_ISSUER_URI}
然后我们来看 OIDC 的几种授权模式。
应用类型
授权模式
有后端场景
授权码模式
SPA,无后端
隐式模式
应安全存储密钥
密码模式
服务器之间
Client Credentials
这个是 Authing 推荐的选择方式,不过据我的经验来讲,就 Web 开发而言大多还是选择隐式授权的居多。看的出来懒猫的 OIDC 也是用的这种。
懒猫微服也是用的授权码模式, 所以跳转的时候我们抓浏览器请求会看到:
1
https://url/callback?code=xxxx
其实一个良好的 OIDC 流程是这样的:
当访问没有权限的路由的时候,在路由守卫中重定向到登录页面。
当用户登录的时候,跳转到对应的的 IDP 控制页面,然后输入用户凭证。这个时候会走 IDP 的认证。
认证之后会颁发一次性 code(授权码模式),如果是简单的密码模式,那么就会直接返回 Access Token,ID token 以及 refresh token。
使用授权码 code 换取 AccessToken、IdToken 以及 refresh token。授权码模式的好处是,把真正的令牌藏在后端交换,只暴露一次性 code,从而极大降低令牌泄露和被滥用的风险。
最后我们再用 AccessToken 来访问资源。
以上是基于懒猫的 OpenID Connect (OIDC)的理论讲解的部分,后面我们会进行实操,手把手创建可以接入 OIDC 的应用。
备注:关于部分 OIDC 的图文来自 Authing 文档。
https://docs.authing.cn/v2/concepts/oidc/choose-flow.html
---
## 京东售后拒修 AirPods 称"无问题",苹果官方检测后直接换新!京东竟删除检测记录?
- URL: https://blog.no-claw.com/posts/c67acea7/
- 日期: 2025-07-08
- 分类: 电子产品 / 电脑 / 苹果
- 标签: Apple
去年在京东买的 Airpod Pro2 售后记录,以前都是在官网买的,还是图省事了。结果闹了个哭笑不得。
故障问题是,只有一边的耳机有声音,于是京东寄修,得到的答案是没问题,直接给我原反了。然后后台给我一个一张检测单(请记住这个,后面京东后台竟然把这个删了)
所谓京东的人给我打电话,是沈阳口音,然后听起来就是一点不懂技术的样子。说耳机是固件的 bug,然后直接把固件降级然后就好了。
然后我直接三连问:
airpod 的固件是自动升级的,用户没办法干预,怎么保证下次自动升级不会再有问题?
有没有什么硬件问题?有时候合上盖子还有声音?
有时候一个耳机掉电快,另外一个基本满电能不能看一看?
口头说好找人看,然后耳机直接给寄回来了,没有任何后续。
京东 Apple 里的耳机检测图片,一看就很不专业,像是某种小店。一口一个有苹果官方的人员主场,一口一个不解决问题,怕不是外包 hhh
但是,寄回来的耳机仍然是一样的问题。再次申请售后是不予受理。理由是上一次检测没有问题,让我去 Apple 线下自行解决。从普通客服到专员,再到所谓的客服经理无一不如此。真是,体验简直太差。那就让我去 Apple 直营店打你们的脸吧。
去了 Apple 三里屯,检测确实有硬件问题,左耳机收音有问题,右耳机能隔着盒子放出来声音,于是给换新了。贴一下检测单。
然后继续去京东 battle,说 apple 给换新了,你们京东为啥检测不出来。然后仍然是不予受理,继续装死。然后默默的把之前检测没问题的单子给删除了。(最前面的一张)
这里的红框圈起来的部分,本来是原来那个检测单的,后来竟然给删了,销毁证据,京东你真行。
以后电子产品大件还怎么放心在京东买啊?
---
## 懒猫微服开发篇(三):如何将已有 Docker Compose 应用移植到懒猫微服
- URL: https://blog.no-claw.com/posts/5e5f8aaa/
- 日期: 2025-07-07
- 分类: 懒猫微服 / 开发
- 标签: 开发, 懒猫微服
本文延续前两篇,演示如何把一个已经在本地运行良好的 Docker Compose 应用打包并上架到懒猫微服应用商店。以 Milvus 为例,逐步拆解 Manifest 配置、路由映射、数据卷绑定以及镜像加速等关键环节,帮助大家快速完成移植。
1. 目录结构与核心文件
在懒猫微服中,一个最小可用的应用包仅需两个文件:
文件
作用
lzc-build.yml
描述打包流程及应用图标。简单应用只需指定 icon 即可。
lzc-manifest.yml
定义应用元数据与服务编排,是移植的重点。
本文主要关注 lzc-manifest.yml 的编写。
2. lzc-manifest.yml 字段逐一解析
现在有了懒猫应用查看器很方便,我们以商店里的 Milvus 的示例 Manifest 为例,并附带注释说明。
这个 lzc-mainfest.yml 解析是重点。主要是 subdomain,ingress,services 这几个字段。总体上还是延续了 Docker compose 的风格。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
lzc-sdk-version: "0.1"
name: Milvus
package: in.zhaoj.milvus
version: 2.5.8
author: milvus
application:
# 子域名:应用上线后将访问 https://milvus.<机器名>.heiyu.space
subdomain: milvus
background_task: false # 是否允许后台运行
multi_instance: false # 是否允许多实例
gpu_accel: false # 是否请求 GPU
routes: # 七层(HTTP)路由
- /=http://attu.in.zhaoj.milvus.lzcapp:3000/
ingress: # 四层(TCP)转发
- protocol: tcp
port: 19530
service: standalone
- protocol: tcp
port: 9091
service: standalone
services: # 以下基本等同于 docker‑compose 中的 services
etcd:
image: registry.lazycat.cloud/longixaoyi/milvusdb/etcd:v3.5.18
environment:
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_AUTO_COMPACTION_RETENTION=1000
- ETCD_QUOTA_BACKEND_BYTES=4294967296
- ETCD_SNAPSHOT_COUNT=50000
command: >
etcd -advertise-client-urls=http://127.0.0.1:2379
-listen-client-urls http://0.0.0.0:2379
--data-dir /etcd
binds:
- /lzcapp/var/etcd:/etcd
minio:
image: registry.lazycat.cloud/longixaoyi/milvusdb/minio:RELEASE.2023-03-20T20-16-18Z
environment:
- MINIO_ACCESS_KEY=minioadmin
- MINIO_SECRET_KEY=minioadmin
command: minio server /minio_data
binds:
- /lzcapp/var/minio_data:/minio_data
health_check:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
standalone:
image: registry.lazycat.cloud/longixaoyi/milvusdb/milvus:v2.6.0-rc1
command: milvus run standalone
environment:
- ETCD_ENDPOINTS=etcd:2379
- MINIO_ADDRESS=minio:9000
depends_on:
- etcd
- minio
binds:
- /lzcapp/var/milvus:/var/lib/milvus
attu:
image: registry.lazycat.cloud/longixaoyi/zilliz/attu:latest
environment:
- MILVUS_URL=standalone:19530
depends_on:
- standalone
2.1 subdomain
subdomain 是应用程序上线的域名,例如上述配置上线后即为 https://milvus.<节点名>.heiyu.space。
2.2 routes
route 来做七层的 HTTP 转发,类似 Nginx 的反向代理。规则格式为 本地路径 = 目标 URL。在示例中,根路径 / 被转发到前端服务 attu 的 3000 端口。
URL 规则:服务名.包名.lzcapp:端口。(包名随意起)
- /=http://attu.in.zhaoj.milvus.lzcapp:3000/
这里 attu 是服务名,端口是 3000,in.zhaoj.milvus 是包名。
2.3 ingress
用于四层直通转发,适用于非 HTTP 协议(数据库、SSH 等)。示例将 Milvus 的 gRPC (19530) 与 HTTP (9091) 端口暴露给外部。
2.4 bind
Manifest 中的绑定路径以 /lzcapp/var 为前缀。发布后会被映射到宿主机的 /data/app/var/,也算是为了简化程序移植和学习成本。和 Docker-compose 写绝对路径来说,这里的可移植性执行更好。
健康检查在这里不是必须的,因为打包的上架的时候服务会帮忙做这个事情。
3. 服务映射与 docker-compose.yml 对照
懒猫 Manifest 的 services 段几乎一一复刻了传统 Compose 配置,常用键均保持一致。以下列出了 Milvus 官方 docker-compose.yml,方便对照理解:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
version: "3.5"
services:
etcd:
container_name: milvus-etcd
image: quay.io/coreos/etcd:v3.5.18
environment:
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_AUTO_COMPACTION_RETENTION=1000
- ETCD_QUOTA_BACKEND_BYTES=4294967296
- ETCD_SNAPSHOT_COUNT=50000
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd
command: etcd -advertise-client-urls=http://etcd:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
healthcheck:
test: ["CMD", "etcdctl", "endpoint", "health"]
interval: 30s
timeout: 20s
retries: 3
minio:
container_name: milvus-minio
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
environment:
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
ports:
- "9001:9001"
- "9000:9000"
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data
command: minio server /minio_data --console-address ":9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
standalone:
container_name: milvus-standalone
image: milvusdb/milvus:v2.6.0-rc1
command: ["milvus", "run", "standalone"]
security_opt:
- seccomp:unconfined
environment:
ETCD_ENDPOINTS: etcd:2379
MINIO_ADDRESS: minio:9000
MQ_TYPE: woodpecker
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]
interval: 30s
start_period: 90s
timeout: 20s
retries: 3
ports:
- "19530:19530"
- "9091:9091"
depends_on:
- "etcd"
- "minio"
networks:
default:
name: milvus
差异点主要体现在:
镜像源:建议通过 lzc-cli appstore copy-image 把镜像同步到 registry.lazycat.cloud,解决国内网络拉取问题。
端口声明:在 Manifest 中,外部访问端口通过 ingress,不再使用 Compose 的 ports。
健康检查:懒猫平台会统一探测容器存活,可按需省略 healthcheck。
4. 镜像加速实践
一条命令即可完成镜像复制并输出新的仓库地址:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
lzc-cli appstore copy-image nginx (base) 13:15:36
Waiting ... ( copy nginx to lazycat offical registry)
uploading
23e05839: [####################################################################################################] 100%
23e05839: [####################################################################################################] 100%
23e05839: [####################################################################################################] 100%
3da95a90: [####################################################################################################] 100%
48670a58: [####################################################################################################] 100%
6c8e51cf: [####################################################################################################] 100%
9bbbd7ee: [####################################################################################################] 100%
ce713206: [####################################################################################################] 100%
ee95256d: [####################################################################################################] 100%
uploaded: registry.lazycat.cloud/u04123229/library/nginx:d037205fbaf7d60a
将生成的地址替换到 Manifest 的 image 字段即可。
5. 小结
Manifest 为核心:lzc-manifest.yml 描述了全部运行时需求,移植时优先完善此文件。
路由分层:routes 管理 HTTP,ingress 管理 TCP,二者配合即可覆盖绝大多数场景。
统一数据目录:使用 /lzcapp/var 避免硬编码路径,便于跨节点迁移。
镜像国内托管:通过 lzc-cli appstore copy-image 自动同步到 LazyCat Registry,稳定又快速。
至此,Milvus 的 Docker Compose 应用已成功移植到懒猫微服。更多进阶玩法,例如 OIDC、VNC 集成等,我们将在后续文章继续分享。
---
## Mac 上 Fn + F12 怎么改成 F12?(适用于 macOS 13 Ventura)
- URL: https://blog.no-claw.com/posts/18eac60e/
- 日期: 2025-07-07
- 分类: 电子产品 / 电脑 / 苹果
- 标签: Apple
很多刚换到 Mac 的用户会遇到一个小困扰:
在键盘上直接按 F12 默认是调节音量,而不是我们常用的 F12 功能键(比如在浏览器里打开开发者工具)。
这时候,就得按 Fn + F12 才能触发真正的 F12。
其实在 macOS 13 Ventura 系统中,我们可以很轻松地把键位逻辑切换过来,让 F12 默认就是 F12,而音量调节则通过 Fn + F12 来实现。下面是详细操作步骤。
设置步骤
打开 **系统设置 (System Settings)**。
(屏幕左上角点苹果图标 → 系统设置)
在左侧列表里找到并点击 **键盘 (Keyboard)**。
在右侧找到 键盘快捷键… (Keyboard Shortcuts…) 按钮,点击进入。
在弹出的窗口中,往下拉到最下面,找到 **功能键 (Function Keys)**。
打开 “将 F1、F2 等键用作标准功能键” (Use F1, F2, etc. keys as standard function keys)开关。
设置后的效果
直接按 F12 → 触发 F12 功能键(开发者工具、应用快捷键等)。
Fn + F12 → 调节音量。
这样一来,就能让 F12 回归它的本职工作,日常办公和开发都方便很多。
小贴士
如果你经常调节音量、亮度,可以保持默认模式更方便;
如果你更常用功能键(F1–F12),建议切换成标准功能键模式;
进阶用户还可以安装 Karabiner-Elements 来做更复杂的键位映射。
📌 结语:
几步设置,就能让你的 Mac 更顺手。如果你也因为 Fn+F12 而烦恼,不妨试试这个小技巧。
---
## 高版本的MacOS如何降级?
- URL: https://blog.no-claw.com/posts/5b6f9bd/
- 日期: 2025-07-07
- 分类: 电子产品 / 电脑 / 苹果
- 标签: Apple
很早问过 Apple 客服 MacOS 的降级机制,半天也没说明白。但是 M 芯片的 Macbook 经常崩溃,如果说 windows 的蓝屏时,那我这个 MacBook 就能崩溃出彩虹色。
寻找过 Apple 支持,答案就是升级系统。Apple 的行政关系团队给我找了一个非常不靠谱的人,一问三不知,问她什么就是再转问工程团队,然后所有的事情都推第三方软件。然后行政关系团队陈某说对技术不做评价,然后一再坚持他们的人都是专业培训上岗的,然后坚持不换人,坚持不解决电脑问题来给客户扣不配合的帽子。
然后一直拖到过保。以前 iphone 接不到电话是这样,现在 Macbook 还是这样。
言归正传。Mac 刷机一般几种办法。
U 盘刷机,这个是传统了,玩过 PE 的都懂。
系统内格式化:就跟手机差不多的那种。个人感觉不彻底。
DFU 刷机:需要你有另外一个 MacOS 的电脑。类似于安卓线刷。
MacOS 降级我采用的是 U 盘装机。参考这个帖子
https://support.apple.com/zh-cn/101578
从 Apple Store 下载 OS,然后把 U 盘的 label 改成/Volumes/MyVolume,最后做随身碟
可以选择各个 MacOS 大版本的最后的 release。
烧录命令是:
1
sudo /Applications/Install\ macOS\ Ventura.app/Contents/Resources/createinstallmedia --volume /Volumes/MyVolume
如果需要其他的系统,那么换一个版本号即可。
我的 23 年的 M2 Pro,当时出长的时候是 MacOS13 Ventura,所以当我想换回 MacOS12 的时候下载都报错。
如果直接安装还会报错:这个卷无法降级。(不理解这个操作,windows 的话随便格式化)
需要进入磁盘工具降级:
然后安装就可可以了,剩下就是漫长的等待。
从 Intel 黑苹果时代走过来的,结果白的还没黑的好用。。。。
---
## 懒猫微服开发篇(二):远程调试之 Devshell
- URL: https://blog.no-claw.com/posts/2c167bb4/
- 日期: 2025-07-06
- 分类: 懒猫微服 / 开发
- 标签: 开发, 懒猫微服
我们上一篇讲述了如何打包上架 APP,这期我们就来看如何远程调试即将上架的 APP。
简单来讲,这个 devshell 其实就是在微服上打开了一个虚拟机环境,然后我们可以进去里面测试我们的命令。非要说技术实现,那就是 docker exec 了。只不过是做成了本地和微服之间同步的样子。
输入微服的名字/v2/_catalog,可以看到微服里面 Docker 仓库存放的镜像,debug.bridge 开头的就是 devshell 的 image。
1
2
3
4
5
6
7
{
"repositories": [
"debug.bridge/231ee20d4e4d8edbd2004e7609fd9c15",
"debug.bridge/ddc6dbf609125b7bd2c0efb0ed4254d1",
"helloworld"
]
}
在 lzc-build.yml 里定义了 devshell 的配置,这个的意思是就是安装 node 和 python 的环境,设置国内源,然后讲根目录转发到 5173,这个是 vue 的端口。所以我们可以推断,这个是一个 Vue+python 的全栈项目,所以我们可以开两个终端来进入 devshell,分别调试前后端。
1
2
3
4
5
6
7
8
9
10
11
devshell:
routes:
- /=http://127.0.0.1:5173
dependencies:
- nodejs
- npm
- python3
- py3-pip
setupscript: |
export npm_config_registry=https://registry.npmmirror.com
export PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
使用 lzc-cli project devshell -f 进入 devshell,可以看到这个使用了 registry.lazycat.cloud/lzc-cli/devshell 作为 base image,然后换源安装依赖。最后设置 setupscript 里面的 ENV。而 CMD [“sleep”, “infinity”]会让容器启动后执行sleep infinity` 命令,即无限休眠。目的是防止容器因没有前台进程而自动退出(保持容器处于“运行”状态)。
这里有个问题,如果你在 lzc-manifest.yml 里指定了 routes 也同样会生效。如果你要跑一些初始化脚本,可以在使用这两个办法
在 lzc-manifest.yml 文件中使用/api/=exec://3000,./lzcapp/pkg/content/backend/run.sh
lzc-build.yml 中注入脚本 setupscript。
如果你偷懒在 lzc-build.yml 里 routes 的执行/api/=exec://3000,./lzcapp/pkg/cache/backend/run.sh,似乎也只能转发端口,不能运行脚本。
从日志看,也就是说还是安装了一个应用程序上去,只不过我们可以通过类似 ssh remote 的方式来动态调试。如果 APP 上架之后,能做的恐怕只有 lzc-docker exec 了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
跳过执行 buildscript
跳过拷贝 contentdir 内容
Dockerfile
STEP 1/4: FROM registry.lazycat.cloud/lzc-cli/devshell:v0.0.5
STEP 2/4: RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
--> Using cache 2302149ded79afb639f9935a07e7ea0d63b5644b10e9890d49ad06786e7b31db
--> 2302149ded79
STEP 3/4: RUN apk add --no-cache bash nodejs npm py3-pip python3 && echo "root:root" | chpasswd
--> Using cache 9bb679fa2c9d10ab1a2433be4c59c852affb2a6844c62ecd9eb8d727505821fb
--> 9bb679fa2c9d
STEP 4/4: CMD ["sleep", "infinity"]
--> Using cache 5ed701af0e0f7040c3dc5409f547b271b4f4e792fd4fcbfc4af8a3abecf8d363
COMMIT debug.bridge/ddc6dbf609125b7bd2c0efb0ed4254d1:latest
--> 5ed701af0e0f
Successfully tagged debug.bridge/ddc6dbf609125b7bd2c0efb0ed4254d1:latest
5ed701af0e0f7040c3dc5409f547b271b4f4e792fd4fcbfc4af8a3abecf8d363
Getting image source signatures
Copying blob sha256:c164879b06ca56693b742ec917059cce495320d4d8f6140bde7e875f53377ea1
Copying blob sha256:69bc8c25bce956e4c34fdfee091c531ffa8660454526ea9f76c956f9b930c57b
Copying blob sha256:98e60e58e2d093fd95b5d5d61ae6dac25bff89ba1b46d42395d480dfb75bddab
Copying blob sha256:d4fc045c9e3a848011de66f34b81f052d4f2c15a17bb196d637e526349601820
Copying blob sha256:b244bd08b327b00bfbbbbf4f424ade914ee891a52d9192a573dbe484e22d86ab
Copying config sha256:5ed701af0e0f7040c3dc5409f547b271b4f4e792fd4fcbfc4af8a3abecf8d363
Writing manifest to image destination
输出lpk包 /Users/xu/Desktop/todolist-py-lzcapp-demo/cloud.lazycat.app.todolistpy-v0.0.1.lpk
开始部署应用
开始安装应用
安装成功!
👉 请在浏览器中访问 https://todolistpy.dev.heiyu.space
👉 并使用微服的用户名和密码登录
+ export 'npm_config_registry=https://registry.npmmirror.com'
+ export 'PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple'
+ set -e
+ mkdir -p /lzcapp/cache/devshell
+ cd /lzcapp/cache/devshell
+ exec /bin/sh
/lzcapp/cache/devshell #
/lzcapp/cache/devshell # ls
README.md backend build.sh lzc-build.yml lzc-icon.png lzc-manifest.yml ui
我们可以看到文件夹的内容已经被同步过来了,我们可以实时同步开发机文件的创建和修改。不过目前和移动,重命名相关的同步有点小问题,具体表现为
本地删除旧的文件,但是 devshell 里不会同步删除
删除文件 a,但是把 b 重名为 a,devshell 不会同步更改(但是删除 a 重建可以)
相信这两个小问题修好也只是时间问题,当然也有 workaround。
从/data/app/cache/包名这个目录删除 devshell 文件夹之后,然后重新执行 lzc-cli project devshell -f ,这样工作区就被清理干净了。
1
2
3
4
5
(base) lzcbox-029c588e /data/app/cache/cloud.lazycat.app.todolistpy # ls
devshell
(base) lzcbox-029c588e /data/app/cache/cloud.lazycat.app.todolistpy # cd devshell/
(base) lzcbox-029c588e /data/app/cache/cloud.lazycat.app.todolistpy/devshell # ls
1 1312 README.md backend build.sh lzc-build.yml lzc-icon.png lzc-manifest.yml ui
另外,我们再通过 ssh 进入/data/app 这个目录,有两个子文件夹,一个叫做 cache,一个叫做 var。 cache 就是我们这个 devshell 的工作区。var 就是数据持久化的目录。如果在这里新建一个目录,那么就可以在网盘里实时看到。
1
/data/app/var/xu.deploy.lazycat-nav # touch test
以上就是 devshell 的全部内容了,如果你需要借助微服的一些特性还进行开发,比如获取环境变量,OIDC 什么的,可以用这个方法来轻松调试。
---
## 懒猫微服实战入门(十八):用 planka 做项目管理
- URL: https://blog.no-claw.com/posts/51800531/
- 日期: 2025-07-06
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服
工作里用 Jira 管理项目进度。家里也是想找个类似的软件来管理一些长期的事情,如果能带一多人合作的功能就更好了。在网上找了很久也没有太好用的,直到在懒猫微服中找到 planka,在对比了几个同类型的软件之后,决定使用这个软件来管理自己的一些事情。
其他的软件体验不佳的点主要是:
付费:还都是订阅制,没有找到终身制的软件
界面太丑,操作太复杂
Saas 免费版本延迟太高
移动端访问体验不佳
Planka 算是解决了大部分的问题:
虽然这个页面不是很现代化风格,但是也不丑,页面响应速度很快。而其他软件大多臃肿体验不佳,要么就是延迟的很高耽误体验。
简介的 UI 和快速的响应很好的诠释了 less is more 的原则。
进入主页之后可以新建多个项目,算是一个隔离吧,虽然我目前也只是用到了一个。
每一个 Project 都能开好几个 board,这样就把不同类别的事情跟区分开了。
然后每个 board 上标注 Todo,Doing,Pending,Done,Deprecated 来确认事情的进度。有点白版卡片的那个味道了,不用自己像线下那种写贴纸来回移动,也不用再花费软件的订阅的费用。
这个是我和前端协作开发时候一起做的 Dashboard,当时在一起开发一个前后端分离的大语言模型的 APP。
只要给他新建一个用户就可以了,然后分给他某个面板的权限,然后就可以可以一起愉快的协作了。(前提给他安装了懒猫微服的客户端,分了 app 权限)
这个是面板的详细参数,可以把 task 分配给某个成员,然后也可以添加 task 描述,task 子任务拆解以及成员评论,拿来记录一些 change log 还是不错的,起码测试的一些过程可以随手记录在这里了。
Dashboard 功能还需要继续探索,比如计时,打标签一些的功能。
不过我目前用到的功能就这么多,也算能基本覆盖全部的场景了。
这个是我用 planka 来追踪关于懒猫微服的写作和上架应用的一些事情,真的帮助了我很多很多。
---
## 懒猫微服开发篇(六):用 Openresty 做反向代理来解决跨域问题
- URL: https://blog.no-claw.com/posts/300338c6/
- 日期: 2025-07-04
- 分类: 懒猫微服 / 开发
- 标签: 懒猫微服, 代理
对于 Web 开发而言, 经常会遇到跨域问题。我们先来看一下什么是跨域问题:
跨域问题(Cross-Origin)本质上是浏览器的同源策略(Same-Origin Policy, SOP)在发挥作用:
同源指“协议 + 域名(或 IP)+ 端口”三要素完全一致。只要三者有任何一个不同,就被视为跨域。
为什么浏览器要限制跨域?
安全:阻止一个站点随意读取或修改另一个站点的敏感资源(如 Cookie、LocalStorage、DOM),避免 XSS、CSRF 等攻击链被无限放大。
隔离:让不同网站在沙盒里各自运行,互不干扰。
同源策略只在浏览器环境生效;后端服务之间(如服务器 A 请求服务器 B)并没有 SOP 的限制。
场景
描述
是否受限
fetch('https://api.foo.com') 从 https://www.bar.com 发出
协议、域名不同
受限
http://example.com:3000 调用 http://example.com:4000
端口不同
受限
⚠️ 用 Nginx/OpenResty 并不会“自动”解决 CORS。
你可以把前端请求代理到后端 API,使浏览器认为请求仍在同一域名下,达到“变同源”的效果。
或者直接在后端/代理层加 CORS 响应头,两种方式都可以。
懒猫微服的上使用的是 OpenResty,这是一个功能齐全的 Web 应用服务器,它集成了标准的 nginx core、大量第三方 nginx 模块以及它们的大部分外部依赖项。所以和 Nginx 的配置文件是通用的。
以我之前比赛做的项目为例,这个是 Nginx 作为网关,监听 80 端口,然后反向代理到 Next.js 和 Flask。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
services:
nginx:
build:
context: .
dockerfile: Dockerfile
ports:
- "80:80"
depends_on:
- next-app
- backend
restart: unless-stopped
next-app:
image: smart-shopping-app
container_name: next-frontend
expose:
- "3000"
environment:
- NODE_ENV=production
- NEXT_TELEMETRY_DISABLED=1
restart: unless-stopped
backend:
image: shoppingassistant-backend
container_name: backend-app
expose:
- "5005"
restart: unless-stopped
而 Nginx 的配置文件如写,做七成的转发,把根路径转发到前端,/api 转发到后端。所以前端的 axios 请求等于访问的/api 这个端点,所以可以规避跨域的问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://next-app:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /api {
proxy_pass http://backend:5005;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
其实对于懒猫微服的 OpenResty 的也是一样的,好处是不用自己再找 base image 了,直接把配置文件写进去就能用了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
lzc-sdk-version: "0.1"
name: APP Proxy Test
package: cloud.lazycat.app.app-proxy-test
version: 0.0.1
application:
routes:
- /=http://app-proxy.cloud.lazycat.app.app-proxy-test.lzcapp:80
subdomain: app-proxy-test #
services:
app-proxy:
image: registry.lazycat.cloud/app-proxy:v0.1.0
setup_script: |
cat <<'EOF' > /etc/nginx/conf.d/default.conf
server {
listen 80;
server_name _;
# 静态页面
location / {
root /usr/local/openresty/nginx/html;
index index.html index.htm;
}
# API 反向代理,保留 /api 前缀
location /api/ {
proxy_pass http://flask:5000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
EOF
flask:
image: registry.lazycat.cloud/u04123229/cloudsmithy/flask-demo:c14689303facd82c
flask 的 image 是我之前做的一个镜像仓库docker run -p 5005:5000 cloudsmithy/flask-demo:latest
然后通过lzc-cli appstore copy-image cloudsmithy/flask-demo 把镜像换成懒猫的镜像,registry.lazycat.cloud/u04123229/cloudsmithy/flask-demo:c14689303facd82c
通过setup_script传入和 Nginx 类似的配置文件,原理是替换 docker image 本身的 entrypoint/command 参数。
1
2
3
4
5
# 打包成 LPK
lzc-cli project build -o release.lpk
# 在线安装 LPK
lzc-cli app install release.lpk
我们可以看到这个是 OpenResty 的主页,然后访问https://app-proxy-test.micro.heiyu.space/api/ 也能返回 Flask 容器“Hello from multi-arch Flask Docker in production mode!”。
如果你想把根路由直接代理到容器,也可以使用这个办法。这个一般是用来做反向代理来访问内网的服务,即使是 http 也没有关系。这个环境变量应该是懒猫魔改的快捷方式。不要和配置文件混用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
lzc-sdk-version: "0.1"
name: APP Proxy Test
package: cloud.lazycat.app.app-proxy-test
version: 0.0.1
application:
routes:
- /=http://app-proxy.cloud.lazycat.app.app-proxy-test.lzcapp:80
subdomain: app-proxy-test #
services:
app-proxy:
image: registry.lazycat.cloud/app-proxy:v0.1.0
environment:
- UPSTREAM="http://flask:5000"
flask:
image: registry.lazycat.cloud/u04123229/cloudsmithy/flask-demo:c14689303facd82c
有时候还会加上BASIC_AUTH_HEADER这个字段来让 nginx/Openresty 自动填写密码,除了你的容器以外,代理外边服务也行。
其实用echo -n "user:password" | base64,的数据来填充BASIC_AUTH_HEADER“Basic “
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
lzc-sdk-version: "0.1"
name: APP Proxy Test
package: cloud.lazycat.app.app-proxy-test
version: 0.0.1
application:
routes:
- /=http://app-proxy.cloud.lazycat.app.app-proxy-test.lzcapp:80
subdomain: app-proxy-test #
services:
app-proxy:
image: registry.lazycat.cloud/app-proxy:v0.1.0
environment:
- UPSTREAM="https://xxx:9200/"
- BASIC_AUTH_HEADER="Basic YWRt46YzssdsfFlOk="
flask:
image: registry.lazycat.cloud/u04123229/cloudsmithy/flask-demo:c14689303facd82c
另外也支持多域名解析,这个在传统的线下机房比较常见,而云上基本上还是 7 层基于路由转发,比如第一种,我也更加熟悉第一种。
这个其实就是加了一个 secondary_domains 的字段,然后把后端单独暴露出来了。这样就子域名就可以访问后端。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
lzc-sdk-version: "0.1"
name: APP Proxy Test
package: cloud.lazycat.app.app-proxy-test
version: 0.0.1
application:
routes:
- /=http://app-proxy.cloud.lazycat.app.app-proxy-test.lzcapp:80
subdomain: app-proxy-test # 应用列表里默认打开的域名
secondary_domains:
- flask
services:
app-proxy:
image: registry.lazycat.cloud/app-proxy:v0.1.0
setup_script: |
cat <<'EOF' > /etc/nginx/conf.d/default.conf
server {
server_name app-proxy-test.*;
location / {
root /usr/local/openresty/nginx/html;
index index.html index.htm;
}
}
server {
server_name flask.*;
location / {
proxy_pass http://flask:5000;
}
}
EOF
flask:
image: registry.lazycat.cloud/u04123229/cloudsmithy/flask-demo:c14689303facd82c
---
## 懒猫微服开发篇(五):懒猫微服如何使用 OpenID Connect (OIDC)?(下)
- URL: https://blog.no-claw.com/posts/48285dc8/
- 日期: 2025-07-04
- 分类: 懒猫微服 / 开发
- 标签: 懒猫微服, 单点登录
先决条件:
需要在lzc-manifest.yml定义 oidc_redirect_path 和 environment。
配置lzc-manifest.yml
oidc_redirect_path 就是你的应用的回调地址,只有写了这个之后才能正确使用 OpenID Connect 的环境变量。
回调地址是按照应用而定的,有的是/callback,/oidc/callback 或者/oauth/callback。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
lzc-sdk-version: 0.1
name: 懒猫ENV查看器
package: xu.deploy.env
version: 0.0.2
description:
license: https://choosealicense.com/licenses/mit/
homepage:
author: xu
application:
subdomain: env
oidc_redirect_path: /callback
routes:
- /=exec://5005,./lzcapp/pkg/content/run.sh
environment:
- LAZYCAT_AUTH_OIDC_CLIENT_ID=${LAZYCAT_AUTH_OIDC_CLIENT_ID}
- LAZYCAT_AUTH_OIDC_CLIENT_SECRET=${LAZYCAT_AUTH_OIDC_CLIENT_SECRET}
- LAZYCAT_AUTH_OIDC_AUTH_URI=${LAZYCAT_AUTH_OIDC_AUTH_URI}
- LAZYCAT_AUTH_OIDC_TOKEN_URI=${LAZYCAT_AUTH_OIDC_TOKEN_URI}
- LAZYCAT_AUTH_OIDC_USERINFO_URI=${LAZYCAT_AUTH_OIDC_USERINFO_URI}
- LAZYCAT_AUTH_OIDC_ISSUER_URI=${LAZYCAT_AUTH_OIDC_ISSUER_URI}
定义了环境变量之后,我们就可以在代码中使用环境变量:
开机时一次性从环境变量读取懒猫微服的应用域名、OIDC 客户端 ID/密钥,以及授权、令牌、用户信息三个核心端点,并根据应用域名拼出默认 Redirect URI,从而把所有与 OpenID Connect 登录相关的敏感信息解耦。
这里的 callback 是应用的回调 URL,需要根据应用调整。
1
2
3
4
5
6
7
8
9
LAZYCAT_BOX_DOMAIN = os.environ.get('LAZYCAT_BOX_DOMAIN')
LAZYCAT_APP_DOMAIN = os.environ.get('LAZYCAT_APP_DOMAIN')
CLIENT_ID = os.getenv("LAZYCAT_AUTH_OIDC_CLIENT_ID")
CLIENT_SECRET = os.getenv("LAZYCAT_AUTH_OIDC_CLIENT_SECRET")
AUTH_ENDPOINT = os.getenv("LAZYCAT_AUTH_OIDC_AUTH_URI")
TOKEN_ENDPOINT = os.getenv("LAZYCAT_AUTH_OIDC_TOKEN_URI")
USERINFO_ENDPOINT = os.getenv("LAZYCAT_AUTH_OIDC_USERINFO_URI")
REDIRECT_URI = os.getenv("OIDC_REDIRECT_URI", f"https://{LAZYCAT_APP_DOMAIN}/callback")
登录功能
在用户访问 /login 时动态生成一对 PKCE 凭据(随机 code verifier 和其 SHA-256 派生的 code challenge),把 verifier 暂存进会话,再携带 challenge 等参数构造 OIDC 授权码请求,并将用户浏览器重定向到身份提供方完成安全登录;回调阶段可用 session 中的 code verifier 与返回的 code exchange 配合,防止授权码被劫持或重放,从而提升 OAuth 2.0/OIDC 的安全性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# ======= 生成 PKCE Code Verifier & Challenge =======
def generate_pkce_pair():
code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b'=').decode()
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b'=').decode()
return code_verifier, code_challenge
@app.route('/login')
def login():
code_verifier, code_challenge = generate_pkce_pair()
session['code_verifier'] = code_verifier
auth_url = (
f"{AUTH_ENDPOINT}"
f"?response_type=code"
f"&client_id={CLIENT_ID}"
f"&redirect_uri={REDIRECT_URI}"
f"&scope=openid profile email"
f"&code_challenge={code_challenge}"
f"&code_challenge_method=S256"
)
print(auth_url)
return redirect(auth_url)
generate_pkce_pair()
使用 secrets.token_bytes(32) 随机生成 32 字节高强度随机数。
先经 base64.urlsafe_b64encode 再去掉尾部 = 得到 code verifier。
对 code verifier 做 SHA-256 散列后再次 base64 URL 安全编码并去掉 =,得到 code challenge。
返回二元组 (code_verifier, code_challenge)。
/login 路由
调用 generate_pkce_pair() 生成并拿到 code_verifier 和 code_challenge。
将 code_verifier 写入 Flask session,以便稍后在回调时校验。
拼接授权端点 AUTH_ENDPOINT 形成认证 URL:
response_type=code 采用授权码模式
client_id、redirect_uri、scope 等常规 OIDC 参数
code_challenge 与 code_challenge_method=S256 声明使用 PKCE(S256)
redirect(auth_url) 将浏览器跳转到身份提供方进行登录 + 授权
回调地址
/callback 处理函数先从回调参数取出授权码,再用会话里的 code verifier 按 PKCE + 授权码模式向令牌端点换取 access token 和 ID token;成功后用 access token 调 /userinfo 获取用户资料,并把三者一起返回。如此既完成了 OAuth 2.0 的安全换码,又拿到了 OIDC 提供的登录身份信息,实现前后端分离的单点登录闭环。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@app.route('/callback')
def callback():
code = request.args.get('code')
code_verifier = session.get('code_verifier')
# 请求 access token
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
data = {
'grant_type': 'authorization_code',
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'code': code,
'redirect_uri': REDIRECT_URI,
'code_verifier': code_verifier,
}
token_resp = requests.post(TOKEN_ENDPOINT, data=data, headers=headers)
print(token_resp)
token_data = token_resp.json()
print(token_data)
access_token = token_data.get('access_token')
id_token = token_data.get('id_token')
# 获取用户信息
userinfo_resp = requests.get(USERINFO_ENDPOINT, headers={
'Authorization': f'Bearer {access_token}'
})
return {
'Access Token': access_token,
'ID Token': id_token,
'UserInfo': userinfo_resp.json()
}
code = request.args.get('code')
从回调 URL 查询参数中取出授权服务器返回的 code(授权码)。
code_verifier = session.get('code_verifier')
读取先前 /login 时存进会话的 code verifier,准备用于 PKCE 校验。
准备换取令牌的 HTTP POST 请求
1
2
3
4
5
6
7
8
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
data = {
'grant_type': 'authorization_code', # 授权码模式
'client_id': CLIENT_ID,
'code': code, # 回调拿到的授权码
'redirect_uri': REDIRECT_URI, # 必须与首跳一致
'code_verifier': code_verifier, # PKCE 关键参数
}
code_verifier 会被身份提供方与首跳收到的 code_challenge 做 SHA-256 对比,从而证明客户端的“持有者”身份,防止授权码被截获后被第三方滥用。
token_resp = requests.post(TOKEN_ENDPOINT, data=data, headers=headers)
向令牌端点发送表单数据以换取 Access Token / ID Token。
token_data = token_resp.json()
解析 JSON 响应。例如:
1
2
3
4
5
6
7
{
"access_token": "...",
"id_token": "...",
"expires_in": 3600,
"token_type": "Bearer",
...
}
access_token = token_data.get('access_token')
读取访问令牌,用于调用受保护 API。
id_token = token_data.get('id_token')
读取 OIDC ID Token,携带用户身份声明,可本地解码验证。
获取用户信息
1
2
3
4
userinfo_resp = requests.get(
USERINFO_ENDPOINT,
headers={'Authorization': f'Bearer {access_token}'}
)
按 OIDC 规范,用 Bearer Token 调 /userinfo 端点,拿到 JSON 形式的用户信息。
返回聚合结果(此处直接返回给浏览器以便演示)
1
2
3
4
5
return {
'Access Token': access_token,
'ID Token': id_token,
'UserInfo': userinfo_resp.json()
}
在浏览器中可以看到这个登录跳转:
还是这个图,我们继续看这个流程:
拿到 code 之后可以就可以换到 Access token 和 ID Token 了,这个 code 只有一次有效,可以达到防重放的效果。当然这个只是 OIDC 的一个例子,在生产环境的 APP 中还需要做路由守卫以及 access 续签的操作。
完整代码如下:
读取配置
从环境变量获取 Lazycat 平台的域名、OIDC 客户端 ID/密钥,以及授权端点、令牌端点、用户信息端点和回调地址。
用随机 app.secret_key 支持 Flask Session。
PKCE 安全增强
generate_pkce_pair() 动态生成 code_verifier / code_challenge ;后者随登录请求携带,前者保存在 Session,回调时再带给 Token 端点,防止授权码被劫持。
核心路由
/:渲染首页(需自备 index.html)。
/login:
生成 PKCE 对;
拼接授权 URL(response_type=code,scope 含 openid profile email);
浏览器重定向到 IdP 登录/授权页面。
/callback:
取回 code 与 code_verifier;
POST 到 TOKEN_ENDPOINT 换取 access_token 和 id_token;
用 access_token 调用 USERINFO_ENDPOINT 拿到用户信息;
以 JSON 形式返回令牌与用户资料。
注意:生产环境应关闭 debug=True、使用 HTTPS、校验 state 参数防 CSRF,并妥善处理 Token 异常和错误分支。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import os
import base64
import hashlib
import secrets
import requests
from flask import Flask, redirect, request, session, url_for, render_template
app = Flask(__name__)
app.secret_key = os.urandom(24)
LAZYCAT_BOX_DOMAIN = os.environ.get('LAZYCAT_BOX_DOMAIN')
LAZYCAT_APP_DOMAIN = os.environ.get('LAZYCAT_APP_DOMAIN')
CLIENT_ID = os.getenv("LAZYCAT_AUTH_OIDC_CLIENT_ID")
CLIENT_SECRET = os.getenv("LAZYCAT_AUTH_OIDC_CLIENT_SECRET")
AUTH_ENDPOINT = os.getenv("LAZYCAT_AUTH_OIDC_AUTH_URI")
TOKEN_ENDPOINT = os.getenv("LAZYCAT_AUTH_OIDC_TOKEN_URI")
USERINFO_ENDPOINT = os.getenv("LAZYCAT_AUTH_OIDC_USERINFO_URI")
REDIRECT_URI = os.getenv("OIDC_REDIRECT_URI", f"https://{LAZYCAT_APP_DOMAIN}/callback")
print(CLIENT_ID)
print(CLIENT_SECRET)
print(AUTH_ENDPOINT)
print(TOKEN_ENDPOINT)
print(USERINFO_ENDPOINT)
print(REDIRECT_URI)
# ======= 生成 PKCE Code Verifier & Challenge =======
def generate_pkce_pair():
code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b'=').decode()
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b'=').decode()
return code_verifier, code_challenge
# ======= 首页 =======
@app.route('/')
def index():
return render_template('index.html')
# ======= 跳转授权页 =======
@app.route('/login')
def login():
code_verifier, code_challenge = generate_pkce_pair()
session['code_verifier'] = code_verifier
auth_url = (
f"{AUTH_ENDPOINT}"
f"?response_type=code"
f"&client_id={CLIENT_ID}"
f"&redirect_uri={REDIRECT_URI}"
f"&scope=openid profile email"
f"&code_challenge={code_challenge}"
f"&code_challenge_method=S256"
)
print(auth_url)
return redirect(auth_url)
# ======= 回调处理 =======
@app.route('/callback')
def callback():
code = request.args.get('code')
code_verifier = session.get('code_verifier')
# 请求 access token
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
data = {
'grant_type': 'authorization_code',
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'code': code,
'redirect_uri': REDIRECT_URI,
'code_verifier': code_verifier,
}
token_resp = requests.post(TOKEN_ENDPOINT, data=data, headers=headers)
print(token_resp)
token_data = token_resp.json()
print(token_data)
access_token = token_data.get('access_token')
id_token = token_data.get('id_token')
# 获取用户信息
userinfo_resp = requests.get(USERINFO_ENDPOINT, headers={
'Authorization': f'Bearer {access_token}'
})
return {
'Access Token': access_token,
'ID Token': id_token,
'UserInfo': userinfo_resp.json()
}
if __name__ == '__main__':
app.run(debug=True)
如果使用 authlib 是这样子的.通过 Authlib 把应用接入 OIDC:
启动时先从环境变量读取并校验客户端 ID、密钥及各端点;
随后注册 OIDC 客户端并自动启用 PKCE。
用户访问 /login 时,服务端生成 nonce 并调用 authorize_redirect() 将浏览器跳转到身份提供方登录,同时在会话里保存随机值;身份提供方完成认证后回调到 /callback,authorize_access_token() 会携带先前的 code 和 code verifier 去换取 access token / ID token,并用保存的 nonce 校验 ID Token 防止重放。
成功后解析得到的声明(用户信息)渲染或写入 Session,即可认为用户已登录。如此利用现成库把 PKCE、状态验证、ID Token 验签等安全细节都交给框架处理,只需少量代码就实现了安全的单点登录闭环。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import os, secrets
from flask import Flask, request, render_template
from authlib.integrations.flask_client import OAuth
LAZYCAT_BOX_DOMAIN = os.environ.get('LAZYCAT_BOX_DOMAIN')
LAZYCAT_APP_DOMAIN = os.environ.get('LAZYCAT_APP_DOMAIN')
CLIENT_ID = os.getenv("LAZYCAT_AUTH_OIDC_CLIENT_ID")
CLIENT_SECRET = os.getenv("LAZYCAT_AUTH_OIDC_CLIENT_SECRET")
AUTH_URI = os.getenv("LAZYCAT_AUTH_OIDC_AUTH_URI")
TOKEN_URI = os.getenv("LAZYCAT_AUTH_OIDC_TOKEN_URI")
USERINFO_URI = os.getenv("LAZYCAT_AUTH_OIDC_USERINFO_URI")
JWKS_URI = os.getenv("OIDC_JWKS_URI", f"https://{LAZYCAT_BOX_DOMAIN}/sys/oauth/keys")
REDIRECT_URI = os.getenv("OIDC_REDIRECT_URI", f"https://{LAZYCAT_APP_DOMAIN}/callback")
required = [CLIENT_ID, CLIENT_SECRET, AUTH_URI, TOKEN_URI, USERINFO_URI, JWKS_URI, REDIRECT_URI]
if not all(required):
missing = [k for k, v in zip(
["OIDC_CLIENT_ID","OIDC_CLIENT_SECRET","OIDC_AUTH_URI",
"OIDC_TOKEN_URI","OIDC_USERINFO_URI","OIDC_JWKS_URI",
"OIDC_REDIRECT_URI"], required) if not v]
raise RuntimeError(f"缺少环境变量: {', '.join(missing)}")
app = Flask(__name__)
app.secret_key = "a-very-secret-key"
oauth = OAuth(app)
oidc = oauth.register(
name="casdoor",
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
authorize_url=AUTH_URI,
access_token_url=TOKEN_URI,
userinfo_endpoint=USERINFO_URI,
jwks_uri=JWKS_URI,
client_kwargs={"scope": "openid profile email"},
redirect_uri=REDIRECT_URI,
)
@app.route("/")
def index():
return render_template("index.html")
@app.route("/login")
def login():
nonce = secrets.token_urlsafe(16)
resp = oidc.authorize_redirect(redirect_uri=REDIRECT_URI, nonce=nonce)
resp.set_cookie("oidc_nonce", nonce, max_age=300, httponly=True)
return resp
@app.route("/callback")
def callback():
token = oidc.authorize_access_token()
nonce = request.cookies.get("oidc_nonce")
claims = oidc.parse_id_token(token, nonce=nonce)
env_vars = sorted(os.environ.items())
return render_template("callback.html",
access_token=token.get("access_token"),
id_token=token.get("id_token"),
user_info=claims,
env_vars=env_vars)
@app.route("/env", endpoint="show_env_html")
def show_env_html():
env_vars = sorted(os.environ.items())
return render_template("env.html", env_vars=env_vars)
if __name__ == "__main__":
app.run(debug=True, port=5005)
---
## 使用 Zilliz Cloud 快速体验 Milvus
- URL: https://blog.no-claw.com/posts/938aef4d/
- 日期: 2025-07-02
- 分类: 软件
- 标签: 向量数据库
在学习 Milvus 向量数据库时,除了本地 Milvus Lite、单机版 Milvus Standalone 或 Milvus on K8s 之外,还可以选择 Zilliz Cloud —— 一种无需部署服务器、零成本上手的托管方案。下面将演示如何申请 Zilliz Cloud 中国区免费套餐并运行官方示例代码。
注册并创建免费集群
打开官网
国内站点:https://zilliz.com.cn/
海外站点:https://zilliz.com/
我们本次实验使用的是国内站点,部署在阿里云,目前可以免费使用。
选择 手机号码 或 邮箱 登录/注册。
进入控制台首页后,点击 Create Cluster 按钮。
在弹窗中选择 Free Tier(免费套餐),数据中心默认为 阿里云 · 杭州。
等待几分钟,集群创建完成后会显示 Endpoint URI、API Token、Cluster ID 等信息,请妥善保存。
运行中
二、连接
安装 milvus-cli:
1
2
pip install uv
uv pip install milvus-cli
终端执行 milvus_cli,进入交互式 CLI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
milvus_cli
__ __ _ _ ____ _ ___
| \/ (_) |_ ___ _ ___ / ___| | |_ _|
| |\/| | | \ \ / / | | / __| | | | | | |
| | | | | |\ V /| |_| \__ \ | |___| |___ | |
|_| |_|_|_| \_/ \__,_|___/ \____|_____|___|
Milvus cli version: 1.0.2
Pymilvus version: 2.5.3
Learn more: https://github.com/zilliztech/milvus_cli.
milvus_cli > connect -uri https://in03-d7b5690fee7bcbf.serverless.ali-cn-hangzhou.cloud.zilliz.com.cn -t 88b738ee492b2ad88d69c166ee587825d546b049dab3a5d8767733a636efec52a62e96b283ab90c24146d5a311696dacd9499fc1
Connect Milvus successfully.
+---------+---------+
| Address | |
| Alias | default |
+---------+---------+
milvus_cli > list databases
+--------------------+
| db_name |
+--------------------+
| db_d7b5690fee7bcbf |
+--------------------+
创建虚拟环境(缺少 3.12 时 uv 会自动下载)
1
2
3
4
5
6
uv venv milvus-py --python 3.12
# 激活环境
source milvus-py/bin/activate # macOS / Linux
# .\milvus-py\Scripts\activate # Windows PowerShell
如果你使用的是 conda 也可以:
1
2
conda create -n milvus-py python==3.12 -y
conda activate milvus-py
克隆仓库
1
git clone https://github.com/zilliztech/cloud-vectordb-examples.git
安装 PyMilvus
1
pip3 install pymilvus==2.5.3
进入 Python 示例目录
1
2
3
cd cloud-vectordb-examples/python
需要注意的是,在开源版本的 Milvus 中,端口号是 9530 / 9091 ,而在 Zilliz cloud 上,端口上是 443.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import configparser
import time
import random
from pymilvus import MilvusClient
from pymilvus import DataType
cfp = configparser.RawConfigParser()
cfp.read('config.ini')
milvus_uri = cfp.get('example', 'uri')
token = cfp.get('example', 'token')
milvus_client = MilvusClient(uri=milvus_uri, token=token)
print(f"Connected to DB: {milvus_uri} successfully")
# Check if the collection exists
collection_name = "book"
check_collection = milvus_client.has_collection(collection_name)
if check_collection:
milvus_client.drop_collection(collection_name)
print(f"Dropped the existing collection {collection_name} successfully")
dim = 64
print("Start to create the collection schema")
schema = milvus_client.create_schema()
schema.add_field("book_id", DataType.INT64, is_primary=True, description="customized primary id")
schema.add_field("word_count", DataType.INT64, description="word count")
schema.add_field("book_intro", DataType.FLOAT_VECTOR, dim=dim, description="book introduction")
print("Start to prepare index parameters with default AUTOINDEX")
index_params = milvus_client.prepare_index_params()
index_params.add_index("book_intro", metric_type="L2")
print(f"Start to create example collection: {collection_name}")
# create collection with the above schema and index parameters, and then load automatically
milvus_client.create_collection(collection_name, schema=schema, index_params=index_params)
collection_property = milvus_client.describe_collection(collection_name)
print("Collection details: %s" % collection_property)
# insert data with customized ids
nb = 1000
insert_rounds = 2
start = 0 # first primary key id
total_rt = 0 # total response time for inert
print(f"Start to insert {nb*insert_rounds} entities into example collection: {collection_name}")
for i in range(insert_rounds):
vector = [random.random() for _ in range(dim)]
rows = [{"book_id": i, "word_count": random.randint(1, 100), "book_intro": vector} for i in range(start, start+nb)]
t0 = time.time()
milvus_client.insert(collection_name, rows)
ins_rt = time.time() - t0
start += nb
total_rt += ins_rt
print(f"Insert completed in {round(total_rt,4)} seconds")
print("Start to flush")
start_flush = time.time()
milvus_client.flush(collection_name)
end_flush = time.time()
print(f"Flush completed in {round(end_flush - start_flush, 4)} seconds")
# search
nq = 3
search_params = {"metric_type": "L2", "params": {"level": 2}}
limit = 2
for i in range(5):
search_vectors = [[random.random() for _ in range(dim)] for _ in range(nq)]
t0 = time.time()
results = milvus_client.search(collection_name,
data=search_vectors,
limit=limit,
search_params=search_params,
anns_field="book_intro")
t1 = time.time()
assert len(results) == nq
assert len(results[0]) == limit
print(f"Search {i} results: {results}")
print(f"Search {i} latency: {round(t1-t0, 4)} seconds")
配置连接信息
在 config.ini 中填入你的集群信息(务必保持格式),⚠️ 切勿把 API Key 提交到公开仓库。
1
2
uri = https://
token =
运行示例脚本
1
python3 hello_zilliz_vectordb.py
运行后可见类似输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Connected to DB: https://in03-d7b5690fee7bcbf.serverless.ali-cn-hangzhou.cloud.zilliz.com.cn successfully
Start to create the collection schema
Start to prepare index parameters with default AUTOINDEX
Start to create example collection: book
Collection details: {'collection_name': 'book', 'auto_id': False, 'num_shards': 1, 'description': '', 'fields': [{'field_id': 100, 'name': 'book_id', 'description': 'customized primary id', 'type': , 'params': {}, 'is_primary': True}, {'field_id': 101, 'name': 'word_count', 'description': 'word count', 'type': , 'params': {}}, {'field_id': 102, 'name': 'book_intro', 'description': 'book introduction', 'type': , 'params': {'dim': 64}}], 'functions': [], 'aliases': [], 'collection_id': 457861707686138665, 'consistency_level': 2, 'properties': {}, 'num_partitions': 1, 'enable_dynamic_field': False}
Start to insert 2000 entities into example collection: book
Insert completed in 0.692 seconds
Start to flush
Flush completed in 3.0984 seconds
Search 0 results: data: ["[{'id': 0, 'distance': 10.547525405883789, 'entity': {}}, {'id': 1, 'distance': 10.547525405883789, 'entity': {}}]", "[{'id': 0, 'distance': 8.913854598999023, 'entity': {}}, {'id': 1, 'distance': 8.913854598999023, 'entity': {}}]", "[{'id': 1000, 'distance': 9.11572551727295, 'entity': {}}, {'id': 1001, 'distance': 9.11572551727295, 'entity': {}}]"] , extra_info: {'cost': 6}
Search 0 latency: 3.4933 seconds
Search 1 results: data: ["[{'id': 0, 'distance': 8.898500442504883, 'entity': {}}, {'id': 1, 'distance': 8.898500442504883, 'entity': {}}]", "[{'id': 0, 'distance': 9.7216157913208, 'entity': {}}, {'id': 1, 'distance': 9.7216157913208, 'entity': {}}]", "[{'id': 1000, 'distance': 8.997819900512695, 'entity': {}}, {'id': 1001, 'distance': 8.997819900512695, 'entity': {}}]"] , extra_info: {'cost': 6}
Search 1 latency: 0.099 seconds
Search 2 results: data: ["[{'id': 0, 'distance': 7.597465515136719, 'entity': {}}, {'id': 1, 'distance': 7.597465515136719, 'entity': {}}]", "[{'id': 0, 'distance': 9.255533218383789, 'entity': {}}, {'id': 1, 'distance': 9.255533218383789, 'entity': {}}]", "[{'id': 0, 'distance': 9.471370697021484, 'entity': {}}, {'id': 1, 'distance': 9.471370697021484, 'entity': {}}]"] , extra_info: {'cost': 6}
Search 2 latency: 0.0677 seconds
Search 3 results: data: ["[{'id': 1000, 'distance': 8.828998565673828, 'entity': {}}, {'id': 1001, 'distance': 8.828998565673828, 'entity': {}}]", "[{'id': 1000, 'distance': 8.66336441040039, 'entity': {}}, {'id': 1001, 'distance': 8.66336441040039, 'entity': {}}]", "[{'id': 0, 'distance': 9.222965240478516, 'entity': {}}, {'id': 1, 'distance': 9.222965240478516, 'entity': {}}]"] , extra_info: {'cost': 6}
Search 3 latency: 0.0722 seconds
Search 4 results: data: ["[{'id': 0, 'distance': 9.342487335205078, 'entity': {}}, {'id': 1, 'distance': 9.342487335205078, 'entity': {}}]", "[{'id': 0, 'distance': 6.45243501663208, 'entity': {}}, {'id': 1, 'distance': 6.45243501663208, 'entity': {}}]", "[{'id': 0, 'distance': 8.369773864746094, 'entity': {}}, {'id': 1, 'distance': 8.369773864746094, 'entity': {}}]"] , extra_info: {'cost': 6}
Search 4 latency: 0.0687 seconds
如果控制台显示如上日志,即表明已成功连接集群、创建 collection 并完成简单的向量检索。
然后我们就可以通过控制台来查看这个新建的索引和数据了。
除此之外,zilliz 还提供了 restapi ,这样我们就可以通过请求 HTTP 来完成数据检索了。
1
2
3
4
5
curl --request POST \
--url https://in03-d7b5690fee7bcbf.serverless.ali-cn-hangzhou.cloud.zilliz.com.cn/v2/vectordb/collections/list \
--header 'accept: application/json' \
--header 'authorization: Bearer ' \
--data '{}'
Python 版本的如下,需要我们把 api-key 作为 bear token 传到请求头里。
1
2
3
4
5
6
7
8
9
10
11
12
import requests
url = "https://in03-d7b5690fee7bcbf.serverless.ali-cn-hangzhou.cloud.zilliz.com.cn/v2/vectordb/collections/list"
payload = "{}"
headers = {
'Authorization': 'Bearer '
}
response = requests.request("POST", url, headers=headers, data=payload)
print(response.text)
同样我们再 Postman 上也可以进行测试,需要注意的是,即使请求体是空的,那么也需要使用 {} 来占位。
在左侧的 api-playground 中,我们可以看到更多的 API 操作,同时还可以直接在浏览器上发送请求。
通过 Zilliz Cloud,我们可以在几分钟内获得一套托管版 Milvus 服务,免去本地运维与资源成本,非常适合作为学习、原型开发或小型应用的向量数据库后端。祝大家玩得开心!
---
## 记年初的武汉游记
- URL: https://blog.no-claw.com/posts/cc35fe6/
- 日期: 2025-07-01
- 分类: 散文随笔
- 标签: 游记
头一天在重庆机场过的夜,到达江汉路步行街已经快中午了。在附近的来菜吃的藕汤,在北京就很爱吃清水亭,有藕的 N 多种做法,那么来到湖北不可不吃。
主要还是特意找问当地的小伙伴要了攻略:
推荐的专门吃东西的几条街是玫瑰街、粮道街、万松园、吉庆街这些。早餐小吃比较丰富,武汉的早餐文化特别浓厚,早餐种类特别丰富,很多碳水炸弹。
正餐可以考虑 来菜,是湖北菜,藕汤是湖北菜最具特色的。文章中下面的信息还是可靠的。
午餐:这个季节湖北的藕汤已经上市,基本上随便一家都很好喝,一定要选筒子骨藕汤,9 孔粉藕炖的那种。怕踩坑可以选一些连锁的,出品有保障,如刘胖子、老村长、艳阳天,不怕排队也可以去夏氏砂锅。
武汉魅族魅友家:https://weibo.com/2709494027/4973440665388967 这是小伙伴自己总结的
武汉推崇藕汤一定要用粉藕,有炒菜的那种,也可以点干煸藕丝,那就是不同的味道了
江汉路的风景不错,虽然是老城区,住宿条件差强人意。但总归风景和视野还是不错的,可以远眺长江,夜景也很美,楼下就是江汉路步行街。
携程上酒店骂声一片,都是吐槽住宿添加太差的。如果介意的话可以选择住对面的武昌。去的时候爆满,其他地区送早餐给升级房间的桔子水晶都没有增值服务了。
早上的江汉路是没什么人的,不过晚上到 12 点都很热闹。每天都是拖着走废的腿一瘸一拐的走回去的,青城山崴的脚,再经历重庆摧残,最后在武汉彻底残了。
武汉没有真正的市中心的概念,基本都是一个片区一个片区这样的。武汉本来就是三个城市合并来的,武昌重政治、汉口重经济、汉阳重工业。所以景区或者玩的地方相对也都比较分散。
早餐可以选严老幺的三鲜豆皮和黑色麻将的热干面。那个新出的三鲜面就不要点了,毕竟不是苏州,三鲜面很难吃!!!
现做的三鲜豆皮,很多人排队,一定要现做现吃才好,这样外壳才是脆的。
山海关路来过个不早的早,每个人拿一个小板凳,5 块钱的热干面,三块钱的藕粉汤,基本早上就吃饱了。
第二天还是骑车到山海关路继续喝了藕汤,这家叫做金三角吊子煨汤。20 块钱一碗,有排骨有莲藕,总体来说比来菜吃的爽一点吧。算是在武汉吃到的第一个拉丝的藕。
山海关路也吃了三鲜豆皮,和毛氏汽水包。不过是在吃不下了,据说这家牛肉面也不错。嗯,来自襄阳。
这个是小伙伴的旅游攻略:
1、黄鹤楼,现在应该在准备春晚分会场,闭园了
2、东湖有空可以看看,东湖绿道、或者磨山风景区也可以锻炼一下,都在东湖片区,哈哈
3、湖北省省博物馆离东湖也很近。越王勾践剑、曾侯乙编钟是镇馆之宝。
4、武汉大学凌波门不用去了,日出日落这个季节都不太合适。武大校园里面还行,建筑风格也很不错。
5、宝通寺、古德寺、归元寺是寺庙,长春观是道观。
6、黎黄陂路、昙华林是文艺青年打卡地。黎黄陂路有很多近代风格的建筑,适合拍照。昙华林有一些文创小礼品吧。
7、江汉路、楚河汉街、武商梦时代是商圈。江汉路建筑风格是近代租界风格,距离江滩近。楚河汉街有新开的 SKP,奢侈品应该相对多一些。武商梦时代是亚洲最大的单体商场。
武汉魅族魅友家:https://weibo.com/2709494027/4973440665388967
武汉博物馆真的很大,需要留一天的时间来看。越王勾践剑需要排队很久。
沿着东湖骑车,慢慢就天黑了,然后发现自己在湖中央凌乱。武大不让进,所以一路都在怀疑自己,一个人在外地,大晚上在人少的地方骑车干嘛。
武汉玩的地方,可以考虑下湖北省博物馆,越王勾践剑和曾侯乙编钟(有时候会有编钟表演)。
东湖也还不错,比较大,春天有樱园,也可以在那边露营。这个季节不是特别推荐,风应该会比较大
江汉路步行街是商圈,仅临武汉江滩。江滩晚上可以看对岸的楼宇和大桥上的灯光表演,比较类似上次在杭州游船的风景
从东湖离开已经是这个点了。
在楚河汉街,基本是本老武汉强行拉倒店里。说没有预制菜,藕汤保拉丝。于是大众点评 L6 送了一个价值 60+的藕汤,然后点了一个干煸藕丝。算是在武汉吃的性价比最高的一顿。果然,大众点评高等级在哪里都吃香。不过后来朋友去,据说是是不再送了。
干煸藕丝很酥脆,虽然说干炸的牛肉不多,但是味道简直无可挑剔。不过一个人吃俩大菜是在是太撑了,还是得找个女朋友一起才好。
第二天又去了梨园。下午的东湖,美的不可方物。(没坐船,来回要七八十,而且要跟着船马上回来)
美的像一幅画。
晚上去的夏氏砂锅,因为谈季加上人少,所以基本没排队。
冬天点着炉火,喝着热气腾腾的藕汤,从上暖到下,还有这个藕是真拉丝呀~
徒步横跨长江大桥,属于又菜又还玩,危险指数 4 颗星,车在走桥在晃,小哥自行车在眼前飘过。
用 Apple Watch 记录下这个过程。
春晚彩排的原因,黄鹤楼进不去。只能在外边听见 1234567 的敲鼓声音,不过很多人也说黄鹤楼在外边看看就好了。
文人的名楼情节,大概是拜崔颢的诗词所赐了。
最后一天来参观国民政府办公厅,现在的南阳大楼被改成了酒店还是饭店一类的场所,只有三楼对外开放。电视剧里的政府办公厅和这个很像。
请看大图 VCR:
还有江汉关博物馆的讲诉曾经的故事,近代的条约,租借等等。。。。
江汉路以及黎黄坡路的租借都改成了经典或者银行,但是我们仍然可以看到过去的岁月。
那,南京得什么样啊?
临走之前吃了,王记牛杂,生滚牛肉热干面,真心不错。
回去写了三天四藕小记:
如果用藕来比作爱情,来菜是初相识,幻想最初的美好,该是粉糯入口即化如亲吻红唇,而非迎合大众变节故作搔首,又道是重口难调。再三思虑下,你终究不是我的头牌。出来乍到幻想要轰轰烈烈的爱情,一次次的修改已不愿再等。
老武汉是路边的艳遇,你听我诉着苦, 又抚着上次的伤口,于是急切的来一场试探性的邀约,再经历试探,喧闹,繁华过后,你把新唤作老。清炖,油炸样样拉丝。本已无可挑剔,可惜你在灯红酒绿,不能常常如愿。
小吃街是日常,简单不精致,朴华粗糙,藕形状不一,大口啃着才舒服,细小的渣碎弃之不要。调侃着来往的路人以及同桌的游客,看着提高嗓门吆喝的店家。同时还能大快朵颐享受着放松而自在的瞬间,虽无钟鸣鼎食,处处诉说着平平淡淡。
夏氏的砂锅是醇厚的,就着炉火,半暗的灯光下,乘上一晚热气腾腾的汤,软糯的藕配着油花花的汤,可以散去一整天的疲惫,这家拉丝也是最多的,点着炉火,就好像无论多晚多累总归有人懂你,等你。汤一定要微烫入口,否则要重新加热到沸腾才行,锅气才是真实的人间烟火。
---
## Gemini-cli 踩坑机:国内环境 , 用过 Google Cloud 如何设置?
- URL: https://blog.no-claw.com/posts/8d2ea5f5/
- 日期: 2025-07-01
- 分类: 软件
- 标签: LLM
Gemini 也发布了类似的 AI 编程产品 Gemini Cli - 开源命令行 AI 工具
而且个人谷歌账号登录就能免费用!
免费额度为每分钟 60 次请求、每天 1000 次请求,是业内最高的免费额度,几乎不会遇到限制。
支持 Google 搜索实时联网,为模型提供外部上下文。
支持 MCP 和扩展,便于功能拓展。
可自定义提示词和指令,适应个人或团队工作流。
可在脚本中非交互式调用,实现自动化和集成。
安装 gemini-cli
先来安装 gemini-cli,其实就是一个 NPM 包。
1
2
npm install -g @google/gemini-cli
gemini
如果不出意外的话,执行之后会闪退。网上说需要设置 TUN 代理,甚至连命令行 export 环境变量也不行。
登录 Google SSO 后仍无法使用
然后登录 Google SSO 验证,页面会显示 Gemini Code Assist 已获得访问您账号的授权。但是其实还是不行。我们继续看。
命令行还是会得到这个报错:
设置 GOOGLE_CLOUD_PROJECT 环境变量
网上基本有这个教程:
用过谷歌云或者 ai studio 的,使用 gemini cli 登陆时可能会有些麻烦,可能要打开 console.cloud.google.com,找到你的 project id,然后设置 GOOGLE_CLOUD_PROJECT 环境变量,使用这种方式打开 gemini cli,就可以用了
然后执行这句,这是环境变量。(临时设置, 仅当前会话有效)
1
export GOOGLE_CLOUD_PROJECT="your-project-id"
如果你想让这个永久生效的话:
1
echo 'export GOOGLE_CLOUD_PROJECT="your-project-id"' >> ~/.zshrc
然后 source ~/.zshrc 就可以了。
报错:API 未启用
有发现新的错,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
[API Error: [{
"error": {
"code": 403,
"message": "Gemini for Google Cloud API has not been used in project xxxxx before or it is disabled. Enable it by visiting
https://console.developers.google.com/apis/api/cloudaicompanion.googleapis.com/overview?project=xxxxxx then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our
systems and retry.",
"errors": [
{
"message": "Gemini for Google Cloud API has not been used in project xxxx before or it is disabled. Enable it by visiting
https://console.developers.google.com/apis/api/cloudaicompanion.googleapis.com/overview?project=xxxx then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our
systems and retry.",
"domain": "usageLimits",
"reason": "accessNotConfigured",
"extendedHelp": "https://console.developers.google.com"
}
],
"status": "PERMISSION_DENIED",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "SERVICE_DISABLED",
"domain": "googleapis.com",
"metadata": {
"activationUrl": "https://console.developers.google.com/apis/api/cloudaicompanion.googleapis.com/overview?project=xxxx",
"containerInfo": "xxxx",
"consumer": "projects/xxxx",
"service": "cloudaicompanion.googleapis.com",
"serviceTitle": "Gemini for Google Cloud API"
}
},
{
"@type": "type.googleapis.com/google.rpc.LocalizedMessage",
"locale": "en-US",
"message": "Gemini for Google Cloud API has not been used in project xxxx before or it is disabled. Enable it by visiting
https://console.developers.google.com/apis/api/cloudaicompanion.googleapis.com/overview?project=xxxx then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our
systems and retry."
},
{
"@type": "type.googleapis.com/google.rpc.Help",
"links": [
{
"description": "Google developers console API activation",
"url": "https://console.developers.google.com/apis/api/cloudaicompanion.googleapis.com/overview?project=xxxx"
}
]
}
]
}
}
]]
打开报错了的网页 https://console.developers.google.com/apis/api/cloudaicompanion.googleapis.com/overview?project=xxxxxx,比如这个,这个网页是和你的 ID 相关的,然后点击启用。
成功运行!
终于可以用了不容易。
---
## 懒猫微服开发篇(一):懒猫微服全栈上架指南,一步打包,一键发布
- URL: https://blog.no-claw.com/posts/ec579c86/
- 日期: 2025-07-01
- 分类: 懒猫微服 / 开发
- 标签: 开发, 懒猫微服
懒猫应用离不开社区的力量,有了各位社区贡献者的支持让懒猫商店的应用越来越丰富。下面示范如何把自己的全栈应用上架到懒猫微服。
官网给出的示例里只有 3 个必备文件:lzc-build.yml、lzc-icon.png、lzc-manifest.yml。
**lzc-icon.png**:应用图标,必须为 PNG。
**lzc-build.yml**:定义打包脚本、输出路径与图标路径。
**lzc-manifest.yml**:应用清单,描述路由规则等。
lzc-build.yml 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 打包预处理,例子里是复制目录,打包前端文件
# 见build.sh这个文件
# rm -rf ./dist
# mkdir -p dist
# 构建后端二进制文件,因为后面写了contentdir是 dist 文件夹,
# 所以dist是打包的上下文
# cp -r backend dist/
# 构建前端,这里就是普通的前端打包命令,只是指定了输出文件夹
# cd ui && npx vite build --emptyOutDir --outDir ../dist/web
buildscript: sh build.sh
# manifest: 指定 lpk 包的 manifest.yml ,一般是这个名字不改
manifest: ./lzc-manifest.yml
# contentdir: 前面把前后端打包到这个目录还是。
contentdir: ./dist
# pkgout: lpk 包的输出路径
pkgout: ./
# icon 指定 lpk 包 icon 的路径路径,如果不指定将会警告
# icon 仅仅允许 png 后缀的文件
icon: ./lzc-icon.png
# dvshell 指定开发依赖的情况,这个我们后面专门来讲讲
# 这种情况下,选用 alpine:latest 作为基础镜像,在 dependencies 中添加所需要的开发依赖即可
# 如果 dependencies 和 build 同时存在,将会优先使用 dependencies
devshell:
routes:
- /=http://127.0.0.1:5173
dependencies:
- nodejs
- npm
- python3
- py3-pip
setupscript: |
export npm_config_registry=https://registry.npmmirror.com
export PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
build.sh 执行完后目录结构大致如下:
dist/backend → 后端(可执行/脚本)
dist/web → 前端(静态文件)
lzc-manifest.yml 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
lzc-sdk-version: 0.1
name: 代办清单Py
package: cloud.lazycat.app.todolistpy
version: 0.0.1
description:
license: https://choosealicense.com/licenses/mit/
homepage:
author:
application:
subdomain: todolistpy
routes:
- /=file:///lzcapp/pkg/content/web
- /api/=exec://3000,./lzcapp/pkg/content/backend/run.sh
routes 这里有三种写法:
file 代表文件,一般是纯静态文件,比如打包后的前端文件,也就是在 build.sh 里面做的打包前端的操作,npm build 之类的,我们在脚本里指定了 content 的路径就是机器上的/lzcapp/pkg/content/,所以/lzcapp/pkg/content/web 也就是刚才的 dist/web。这个的意思就是说把根路由转发这个静态目录,其实就是类似 Nginx 托管静态文件这个样子,只是不需要手动打包,写好命令之后,打包工具帮忙做了这一套。
http(s)://$hostname/$path, 这个是我们印象里的网关代理后端服务,比如/api/=http(s)://$hostname/$path,其实就类似 Nginx 的 proxy_pass 将/api 转发到 http(s)://$hostname/$path/。
exec:这个和 http(s)很像,后面多加了一个 run.sh,相当于在转发到 http(s)路由之前,先执行这个脚本。一般是用来预置环境,比如 pip install 什么的,但是由于每个人的环境不一样,还是要使用多个镜像源才保险,我上架的应用就遇到用户通过清华源下载报错 HTTP403 以及 腾讯源下载签名不匹配的问题,或者干脆使用 Docker,这个我们后面再说。
附上 pip 多源的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 已有 —— 主索引 & 前两级备用
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip config --add global.extra-index-url https://pypi.mirrors.ustc.edu.cn/simple/
pip config --add global.extra-index-url https://mirrors.bfsu.edu.cn/pypi/web/simple/
# ③ 阿里云(华东节点评测最稳)
pip config --add global.extra-index-url https://mirrors.aliyun.com/pypi/simple/
# ④ 华为云(华南线路友好)
pip config --add global.extra-index-url https://repo.huaweicloud.com/repository/pypi/simple/
# ⑤ 字节跳动开源镜像(火山引擎,带全站 CDN)
pip config --add global.extra-index-url https://mirrors.byteimg.com/pypi/simple/
# ⑥ 南京大学镜像(NJU,教育网 & 华东建议保留)
pip config --add global.extra-index-url https://mirrors.nju.edu.cn/pypi/web/simple/
打包与安装
1
2
3
4
5
# 打包成 LPK
lzc-cli project build -o release.lpk
# 在线安装 LPK
lzc-cli app install release.lpk
然后是打包,如果缺少 lzc-build.yml,lzc-icon.png,lzc-manifest.yml 三者之一就会报错。
LPK 是懒猫微服应用商店 APP 的安装包格式,其实可以理解为一个配置文件的压缩包,安装之后其实就是在微服内部启动了一个 alpine 的 image,然后通过 build.sh 安装依赖。
通过 lzc-docker 来看,直接打包的就是这个 images registry.lazycat.cloud/lzc/lzcapp:3.20.3
命令如下lzc-docker history –no-trunc registry.lazycat.cloud/lzc/lzcapp:3.20.3,能够看到是 Alpine 作为 base image,然后更换中科大的源,以及安装 gcompat 以兼容 glibc 程序。
1
2
3
4
5
6
7
(base) lzcbox-029c588e ~ # lzc-docker history --no-trunc registry.lazycat.cloud/lzc/lzcapp:3.20.3
IMAGE CREATED CREATED BY SIZE COMMENT
sha256:ba7a533c869a26d89e83bdc5ddb978df5a3502ac91452422a649d0d3cf52190b 7 months ago RUN /bin/sh -c apk add gcompat # buildkit 2.48MB buildkit.dockerfile.v0
7 months ago RUN /bin/sh -c sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories # buildkit 97B buildkit.dockerfile.v0
9 months ago CMD ["/bin/sh"] 0B buildkit.dockerfile.v0
9 months ago ADD alpine-minirootfs-3.20.3-x86_64.tar.gz / # buildkit 7.8MB buildkit.dockerfile.v0
(base) lzcbox-029c588e ~ #
甚至可以看到,这个 image 是连 bash 以及各种开发运行时都没有的。
1
2
3
4
5
6
7
8
9
10
11
12
(base) lzcbox-029c588e ~ # lzc-docker run -it registry.lazycat.cloud/lzc/lzcapp:3.20.3 bash
docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: "bash": executable file not found in $PATH: unknown.
(base) lzcbox-029c588e ~ # lzc-docker run -it registry.lazycat.cloud/lzc/lzcapp:3.20.3 sh
/ # go
sh: go: not found
/ # npm
sh: npm: not found
/ # pip
sh: pip: not found
/ # python
sh: python: not found
/ #
所以这个 backend 文件夹的 run.sh 是拿来安装 Python 依赖的。而前端是使用本地的 npm 打包的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/sh
# 切换到当前目录
cd "$(dirname "$0")"
sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
apk update
apk add python3 py3-pip
# ❶ 设主索引,只能有一个
# 已有 —— 主索引 & 前两级备用
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip config --add global.extra-index-url https://pypi.mirrors.ustc.edu.cn/simple/
pip config --add global.extra-index-url https://mirrors.bfsu.edu.cn/pypi/web/simple/
# ③ 阿里云(华东节点评测最稳)
pip config --add global.extra-index-url https://mirrors.aliyun.com/pypi/simple/
# ④ 华为云(华南线路友好)
pip config --add global.extra-index-url https://repo.huaweicloud.com/repository/pypi/simple/
# ⑤ 字节跳动开源镜像(火山引擎,带全站 CDN)
pip config --add global.extra-index-url https://mirrors.byteimg.com/pypi/simple/
# ⑥ 南京大学镜像(NJU,教育网 & 华东建议保留)
pip config --add global.extra-index-url https://mirrors.nju.edu.cn/pypi/web/simple/
pip install -r ./requirements.txt --break-system-packages
python3 app.py
安装之后的 app 可以通过 lzc-docker 查看,也可以使用 Dozze 查看日志,一般 debug 时候的时候会看这个。
DOZZL 需要安装开发者工具,然后使用https://dev.设备名.heiyu.space/dozzle/访问。
一般来说部署有两个 pod,一个是 App-1 结尾的,主要是涉及到转发,run.sh 自动安装依赖,以及健康检查。
1
2
3
PATH:"/" is served by "file"://"/lzcapp/pkg/content/dist"
PATH:"/api/" is served by "http"://"host.lzcapp:53443"
health check finished
应用名字-1 结尾的,这个才是应用的日志。
1
2
3
4
[2025-06-29 17:29:29 +0800] [1] [INFO] Starting gunicorn 23.0.0
[2025-06-29 17:29:29 +0800] [1] [INFO] Listening at: http://0.0.0.0:9527 (1)
[2025-06-29 17:29:29 +0800] [1] [INFO] Using worker: sync
[2025-06-29 17:29:29 +0800] [9] [INFO] Booting worker with pid: 9
希望大家都能够多多为懒猫微服贡献应用。
---
## 通过 SMTP 使用 163 邮箱发送邮件
- URL: https://blog.no-claw.com/posts/5dfd4b8a/
- 日期: 2025-06-30
- 分类: 软件
- 标签: Python
这里的 授权码(Authorization Code) 是 163 邮箱(以及 QQ 邮箱等国内常见邮箱服务商)专门为 SMTP/POP3/IMAP 等邮件协议提供的独立密码,与邮箱的登录密码不同。
作用:用于通过第三方客户端(如 Python 的 smtplib)发送邮件,避免直接暴露邮箱登录密码。
获取方式(以 163 邮箱为例):
登录 163 邮箱。
进入 设置 → POP3/SMTP/IMAP。
开启 SMTP 服务,系统会提示你设置授权码(类似 ABCDEFG123456,不是你的登录密码)。
复制这个授权码,替换代码中的 your_authorization_code。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
def send_163_email():
# 163邮箱SMTP配置
smtp_server = "smtp.163.com"
smtp_port = 465 # SSL加密端口
sender = "your_username@163.com" # 你的163邮箱
password = "ABCDEFG123456" # 替换为你的SMTP授权码(不是登录密码!)
receiver = "recipient@example.com" # 收件人邮箱
# 创建邮件
msg = MIMEMultipart()
msg['From'] = sender
msg['To'] = receiver
msg['Subject'] = "测试邮件(带Markdown附件)"
# 邮件正文
msg.attach(MIMEText("这是邮件正文,附件是Markdown文件。", 'plain', 'utf-8'))
# 添加Markdown附件
markdown_content = "# CSDN文章汇总\n| 标题 | 链接 |\n|------|------|\n| [Python教程] | https://example.com |"
attachment = MIMEApplication(markdown_content.encode('utf-8'), Name="articles.md")
attachment['Content-Disposition'] = 'attachment; filename="articles.md"'
msg.attach(attachment)
# 发送邮件
try:
with smtplib.SMTP_SSL(smtp_server, smtp_port) as server:
server.login(sender, password)
server.sendmail(sender, receiver, msg.as_string())
print("邮件发送成功!")
except Exception as e:
print(f"发送失败: {e}")
if __name__ == "__main__":
send_163_email()
---
## 懒猫微服实战入门(十七):把 Steam 游戏存到懒猫网盘
- URL: https://blog.no-claw.com/posts/ffc71b6/
- 日期: 2025-06-30
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服
懒猫微服的网盘基本可以替代一些公有的网盘,还有一个好处是可以自动挂载,只要打开懒猫微服的客户端,然后自动把网盘就能自动挂载到 Finder,十分方便。
网盘的挂载是支持了 WebDAV 和 Sambda。这是常见的两种远程访问的协议。
WebDAV(Web Distributed Authoring and Versioning)主要用于 HTTP/HTTPS 协议 的文件共享,适合 远程访问、云存储。基于 HTTP/HTTPS,可在浏览器中直接访问(如 http://server/webdav)。
SMB(Server Message Block)/ Samba 主要用于 局域网文件共享(如 Windows 共享文件夹、NAS、企业内网存储)。
通过 mount 命令可以看到,懒猫微服客户端默认使用的是 SMB 协议挂载:
1
2
//xu-automount@file.micro.heiyu.space/xu-automount on /Users/xu/lazycat_automount/micro (smbfs, nodev, nosuid, mounted by xu)
//xu-automount@file.micro.heiyu.space/xu on /Volumes/xu (smbfs, nodev, nosuid, mounted by xu)
默认有两个文件夹,其实都是指向网盘根目录的软连接。两者内容完全一样。
<用户名>-automount:这个应该不是自动挂载到文件管理器的目录
<用户名>: 这个是网盘多租户的目录
所以在 Steam 里我们直接添加驱动器就好:
然后可以选择这两个文件目录,就像前面介绍的,这两个选择哪个都行:
然后这个时候会弹出来这个提示。我们选择允许。这样 Steam 才能有写入懒猫网盘的权限。
然后选择下载之后,我们发现网盘里多了一个 SteamLibrary 的目录。所有的游戏都存在这里。
如果你比较习惯网盘的页面,也可以在网盘里找到 SteamLibrary 的目录
如果哪天不再需要写入懒猫网盘,用这个办法删除。网上吐槽挺多的。
---
## 懒猫微服实战入门(十六):把懒猫微服当作 24 × 7 在线开发机
- URL: https://blog.no-claw.com/posts/25ccd3a3/
- 日期: 2025-06-28
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服
最近和极限科技沟通,将 INFINI Console 上架到懒猫微服。可以当作随时可用、不关机的 远程开发机。比如连接自己部署的 ES,中间件,数据库什么的。
1. 应用商店一键安装 INFINI Console
从懒猫微服应用商店搜索 Console,点击“安装”即可:
2. 初始化与登录
初始化之后登录:
首次登录后台,左侧侧边栏包含 Dashboard、Agents、Settings 等模块:
Dashboard 默认展示 CPU / 内存 / 磁盘实时曲线。
顶栏可切换“明暗主题”并显示当前工作区 ID。
右下角有“检查更新”按钮,提示有新版时可一键升级。
3. 用 Dockge 安装 Easysearch
ES 的话,我是直接用 Dockge 安装的,如果你需要啥中间价,数据库都可以用这个安装。
前提需要用 lzc-cli appstore copy-image 来获取国内的镜像源:
1
2
# 将官方镜像复制到懒猫内网仓库
lzc-cli appstore copy-image infinilabs/easysearch:1.13.0-2159
然后把 docker run 或 docker-compose.yml 中的镜像地址替换成上一步生成的私有 registry 地址即可。全部容器由 Dockge 图形化管理:
(截图信息要点)
Dockge 左侧列出所有 Stack,右侧显示 Easysearch 服务状态为 Running。
端口 9200 已自动映射,重启、查看日志,都能一键完成。
4. 为什么说它适合做开发机?
需求
懒猫微服能力
体验亮点
24 × 7 在线
独立云主机,自动重启、监控告警
关掉本地电脑,服务仍在运行
x86 架构
后端统一使用 x86 节点
对 Mac M 系列(ARM)用户,可避免本地编译兼容性问题
多端远程开发
内置 Web Shell、端口映射、域名分配
VS Code Remote / JetBrains Gateway 秒连接
镜像同步
lzc-cli appstore copy-image
国内网络下拉镜像不超时
中间件生态
Dockge + Compose
RabbitMQ、Redis、Postgres 都能一键启动
环境变量管理
UI + .env 托管
私密信息集中维护,避免泄漏
总结下来,把懒猫微服当作一个可远程访问的轻量开发机还是挺合适的:
不用担心公网 IP 和端口映射
应用商店部署方便快捷
支持命令行部署、私有镜像同步
用 Dockge 管理一套中间件生态完全没问题
适合:
👉 想要随时随地调试项目的开发者
👉 不想在本地装一堆环境的轻量用户
👉 有多端共享、协作需求的远程开发场景
整套流程走下来,你只需一台浏览器,就能获得 24 × 7 不关机的云端开发环境。如果你也是 Mac M-芯片用户、经常出差或需要多端协作,不妨试试用懒猫微服托管自己的 DevBox。
---
## Easysearch 实战指南:修改索引主分片的三种方式(split shrink reindex
- URL: https://blog.no-claw.com/posts/cba11cdd/
- 日期: 2025-06-27
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
在 Easysearch(兼容 Elasticsearch)的架构中,索引的主分片数(index.number_of_shards)一旦创建就无法直接修改。这给实际使用带来挑战:
设得太少,查询/写入瓶颈出现;
设得太多,资源浪费、集群不稳;
想变更结构,却发现配置是“写死”的。
本文将带你深入了解三种常见但本质不同的索引重构方式:split、shrink、reindex,教你如何选择合适方案、安全操作,并解释为什么split + shrink 无法取代 reindex。
📌 一张图概览三种方式
方法
是否重建索引
可否原名使用
改分片数限制
是否保留数据
是否改结构(mapping/settings)
常见用途
split
✅ 新建索引
❌ 不支持
只能 × 倍数(如 1→2→4)
✅ 是
❌ 否
提升写入并发/读性能
shrink
✅ 新建索引
❌ 不支持
只能 ÷ 因数(如 4→2→1)
✅ 是
❌ 否
合并历史数据分片
reindex
✅ 新建索引
✅ 支持(先删)
任意
✅ 是
✅ 支持
自定义结构/分片/升级
🔧 一、split:将分片数量倍增(如 1 → 2 → 4)
适用于: 提升并发能力、增加查询/写入并行度。
✅ 条件要求:
原始索引必须设置 index.blocks.write: true(只读);主要是防止写入继续增长。
新分片数必须是原主分片的 倍数;
不能使用原名,目标索引必须另起新名。
🛠 操作示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 设置只读
PUT /abc/_settings
{
"settings": {
"index.blocks.write": true
}
}
如果不设置为只读的话,就报错:
```json
{
"error": {
"root_cause": [
{
"type": "illegal_state_exception",
"reason": "index abc must be read-only to resize index. use \"index.blocks.write=true\""
}
],
"type": "illegal_state_exception",
"reason": "index abc must be read-only to resize index. use \"index.blocks.write=true\""
},
"status": 500
}
拆分索引(1 → 4)
1
2
3
4
5
6
POST /abc/_split/abc_split_2shards
{
"settings": {
"index.number_of_shards": 2
}
}
执行结果如下:
1
2
3
4
5
{
"acknowledged": true,
"shards_acknowledged": true,
"index": "abc_split_2shards"
}
如果不是倍数也会报错:
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"error": {
"root_cause": [
{
"type": "illegal_argument_exception",
"reason": "the number of source shards [13] must be a factor of [25]"
}
],
"type": "illegal_argument_exception",
"reason": "the number of source shards [13] must be a factor of [25]"
},
"status": 400
}
查看索引的信息:
1
GET /abc_split_2shards/_settings?flat_settings=true
/_settings:Elasticsearch 提供的 API 端点,用于查看索引设置。
?flat_settings=true:查询参数,使返回结果以扁平化的键值对形式展示(而非嵌套结构)。
可以看到目标的索引也是只读的,这在 Easysearch 里是 ElasticSearch 不一样的地方。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"abc_split_2shards": {
"settings": {
"index.blocks.write": "true",
"index.creation_date": "1750747232004",
"index.number_of_replicas": "1",
"index.number_of_shards": "2",
"index.provided_name": "abc_split_2shards",
"index.resize.source.name": "abc",
"index.resize.source.uuid": "3NY_W5B_TzimoEGdoA74cg",
"index.routing.allocation.initial_recovery._id": null,
"index.routing_partition_size": "1",
"index.uuid": "e2BQiTRKTlaTS5OE8kmiXw",
"index.version.created": "1130099",
"index.version.upgraded": "1130099"
}
}
}
然后使用这个来解锁 write block。
1
2
3
4
5
6
PUT /abc_split_2shards/_settings
{
"settings": {
"index.blocks.write": false
}
}
如果你不想让目标索引变成只读。也可以在_split 的时候加上 “index.blocks.write”: false。
1
2
3
4
5
6
7
8
POST /abc_split_2sharxds/_split/qwe
{
"settings": {
"index.blocks.write": false,
"index.number_of_shards": 26
}
}
🔧 二、shrink:将分片数量整除压缩(如 8 → 4 → 1)
适用于: 历史归档数据压缩、节省内存、提升查询效率。
✅ 条件要求:
所有主分片必须集中在同一节点;
原索引必须只读;
新分片数必须是旧分片数的 因数;
同样不能保留原名,需新建索引名。
🛠 操作示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 强制所有主分片调度到 node-1
PUT /source_index/_settings
{
"settings": {
"index.blocks.write": true,
"index.routing.allocation.require._name": "node-1",
"index.number_of_replicas": 0
}
}
如果不是只读同样报错:
```json
{
"error": {
"root_cause": [
{
"type": "illegal_state_exception",
"reason": "index test1 must be read-only to resize index. use \"index.blocks.write=true\""
}
],
"type": "illegal_state_exception",
"reason": "index test1 must be read-only to resize index. use \"index.blocks.write=true\""
},
"status": 500
}
合并为一个分片
1
2
3
4
5
6
7
POST /source_index/_shrink/source_index_1
{
"settings": {
"index.blocks.write": false,
"index.number_of_shards": 1
}
}
解锁
1
2
3
4
5
6
PUT /source_index_1/_settings
{
"settings": {
"index.blocks.write": false
}
}
🔧 三、reindex:拷贝数据 + 新建结构 + 替换旧索引
适用于: 任意修改分片数、字段结构、settings,或实现“看起来改了原索引”的效果。
✅ 优势:
唯一支持任意分片数修改;
可自由重构 mapping、settings;
可支持保留原名(删除旧索引 + 重新创建);
可带条件、分页、脚本拷贝数据;
是唯一可模拟“修改原索引分片”的方式。
🛠 操作步骤(保留原名但改变结构):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 1. 创建临时索引结构(你想要的新结构)
PUT /my_index_v2
{
"settings": {
"index.number_of_shards": 5,
"index.number_of_replicas": 1
},
"mappings": {
"properties": {
"user": { "type": "keyword" },
"message": { "type": "text" }
}
}
}
# 2. 拷贝数据
POST /_reindex
{
"source": { "index": "my_index" },
"dest": { "index": "my_index_v2" }
}
# 3. 删除旧索引(谨慎)
DELETE /my_index
# 4. 创建同名索引(新结构)
PUT /my_index
{
"settings": {
"index.number_of_shards": 3,
"index.number_of_replicas": 1
},
"mappings": {
"properties": {
"user": { "type": "keyword" },
"message": { "type": "text" }
}
}
}
# 5. 再次拷贝数据(回填)
POST /_reindex
{
"source": { "index": "my_index_v2" },
"dest": { "index": "my_index" }
}
6. 查看索引,并且删除目标索引
GET _cat/indices/my_index*?v

最后删除my_index_v2 即可
DELETE /my_index_v2
⚠️ 为什么 shrink + split 不能替代 reindex?
很多用户会问:能不能 shrink → split 或 split → shrink 拼接出任意分片数?
答案是:数学上不成立 + 实战限制太多。
操作
说明
split 只能倍增
例如:1 → 2 → 4 → 8 ✅,但不能变成 3、5、6 ❌
shrink 只能整除
例如:8 → 4 → 2 → 1 ✅,但不能变成 3、5 ❌
二者组合
受限于倍数 × 因数关系,不是万能变换(大多数目标分片数根本到不了)
✅ 唯一万能方式:reindex
可以任意:
调整分片数 ✅
修改字段结构 ✅
改 settings ✅
保留索引名 ✅
✅ 最佳实践总结
场景
推荐方式
理由
写入并发不足(1 → 4)
split
快速、低风险
存储/查询优化(8 → 1)
shrink
节省资源、适合归档
修改索引结构、字段、settings
reindex
最灵活、唯一支持任意结构
想保留原名但改分片数
reindex(配合 delete/recreate)
只有它能实现
不想中断服务
reindex + alias 切换
alias 实现无缝替换
🚀 附加建议
S* split/shrink 一般用于 线上小范围结构调整;
reindex 用于 升级、清洗、结构优化等更大粒度的改造;
如果你不想中断服务,强烈建议使用 alias + reindex 做平滑切换;
不建议用 shrink + split 拼接方案,实际运维性差、数学关系苛刻。E
---
## 懒猫微服进阶心得(十):本地开发,如何接入懒猫微服的 OpenID Connect (OIDC)
- URL: https://blog.no-claw.com/posts/4daab20a/
- 日期: 2025-06-26
- 分类: 懒猫微服 / 进阶
- 标签: 懒猫微服, 单点登录
我们知道懒猫的 OpenID Connect (OIDC) 无需在后台申请,商店里的应用在运行的时候会自动申请,但是本地测试的时候就不太方便。
一般是需要用其他的 IDP 作为测试环境,因为 OIDC 的协议是通用的,不像 OAuth 这么百花齐放。
以我的“家庭任务通知”APP 为例,讲解下在开发模式下接入懒猫微服的 OpenID Connect (OIDC)。
添加 OIDC 登录逻辑
首先前端需要有一个 OIDC 的登录按钮,然后做好 OIDC 的逻辑:
使用懒猫 ENV 查看器导出本地配置
从应用商店安装我写的“懒猫 ENV 查看器”,导出 env.example 文件,导出项目之后重命名为 .env。这样就可以把商店里的 ENV 复制到本地的开发环境。
不过需要注意的是:应用名字和回调函数还是原来的,不要轻易去改。遇到问题再手动调试。
https://appstore.lazycat.cloud/#/shop/detail/xu.deploy.env
登录后出现回调 URL 报错
登录之后我们就看到了这个页面:
点击“授予权限”,会报错。这个是由于回调 URL 不匹配的问题,还是会访问 ENV 查看器的 URL:
手动修改回调 URL
然后我们手动把上边的 URL 改成我们自己的回调路由就可以了,如果想自动化,你也可以写一个油猴脚本。
这样就可以完成本地的 OIDC 授权流程啦。开发的时候不用搭 IDP,也能走懒猫的登录流程。是不是很方便?
---
## Lighting 原生不支持OTG!
- URL: https://blog.no-claw.com/posts/d969a069/
- 日期: 2025-06-25
- 分类: 电子产品 / 电脑 / 苹果
- 标签: Apple
我还留着 Lighting 的设备,由于大疆 pocket3 最近更新了直连手机文件管理器的功能,所以试试。
用了 Apple 的原装线连接 pocket3 和 iPhone 结果没反应,但是在 IPAD 上就能够成正常连接。
然后换了移动硬盘连接 IPhone 同样也不行,和 IPhone 的售后 battle 了好久对方也说不明白。最后还是 Apple 论坛上找找到一个帖子,
Lighting 原生不支持 OTG,还得买转接头,这算盘打的真响。
https://discussionschinese.apple.com/thread/252659031?=undefined&previousThread=255169304321&sortBy=rank
最后感谢欧盟,要求 Apple 更换 Typec 接口,解决了 USB2.0 的问题,也可以和安卓设备一样用高速传输了。
---
## 懒猫微服排查篇(二):上架应用后,pip 安装报错 HASH 不一致
- URL: https://blog.no-claw.com/posts/cf7f5d3/
- 日期: 2025-06-23
- 分类: 懒猫微服 / 排查
- 标签: Python, 懒猫微服
懒猫 ENV 查看器第二版更新的时候,审核人员和我说遇到了这个错误。说来也奇怪,都用了 docker 了,也会遇到依赖的问题。
https://appstore.lazycat.cloud/#/shop/detail/xu.deploy.env
ERROR: THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE
Expected sha256 4ceb…
Got 5519987f…
因为 pip 在校验阶段就失败,后面的 Flask 等依赖都没装上,于是程序启动时报 ModuleNotFoundError: No module named ‘flask’。
最后我还是替换掉了腾讯云。以清华源为主,其他源为辅:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/sh
# 切换到当前目录
cd "$(dirname "$0")"
sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
apk update
apk add python3 py3-pip
# 设一条主索引(可选)
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
# 给同一个键追加多条 extra-index-url
pip config set global.extra-index-url https://mirrors.aliyun.com/pypi/simple/
pip config set global.extra-index-url https://repo.huaweicloud.com/repository/pypi/simple/
pip config set global.extra-index-url https://mirrors.cloud.tencent.com/pypi/simple/
pip install -r ./requirements.txt --break-system-packages
python3 app.py
网上还有几种办法,后面再遇到的时候可以再尝试:
–no-cache-dir
pip cache purge
pip install –trusted-host=pypi.org –trusted-host=files.pythonhosted.org example_package
rm ~/.cache/pip -rf
关闭机器代理
参考链接:
https://stackoverflow.com/questions/71435874/pip-these-packages-do-not-match-the-hashes-from-the-requirements-file
---
## Easysearch 索引关闭与重开全攻略:open close 操作、批量处理及防误操作配置
- URL: https://blog.no-claw.com/posts/af48f3e/
- 日期: 2025-06-23
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
在 Easysearch(兼容 Elasticsearch 的搜索引擎)中,索引是存储和查询的基本单元。默认情况下,索引是处于 open 状态的,可以正常写入和搜索。当你暂时不使用某些索引,但又不想删除它们时,可以通过 close 操作来关闭索引,从而释放部分内存资源。
📊 查看索引状态
使用以下命令可以查看当前集群中所有索引的状态:
1
GET _cat/indices?v
创建一个索引并插入数据:
1
2
3
4
POST abc/_doc
{
"a": 1
}
此时你会看到索引 abc 已创建,并处于 open 状态:
默认每个索引有 1 个主分片、1 个副本分片,且为可读写状态。
🔒 关闭索引
如果你暂时不需要某个索引,又不希望删除它,可以将其关闭:
1
POST abc/_close
返回结果:
1
2
3
4
5
6
7
8
9
{
"acknowledged": true,
"shards_acknowledged": true,
"indices": {
"abc": {
"closed": true
}
}
}
🚫 关闭后的行为限制
关闭索引后,不仅不能写入,连搜索都无法进行。
🔍 搜索已关闭索引(403 错误)
1
GET abc/_search
返回:
1
2
3
4
5
6
7
{
"error": {
"type": "cluster_block_exception",
"reason": "index [abc] blocked by: [FORBIDDEN/4/index closed];"
},
"status": 403
}
📝 写入已关闭索引(400 错误)
1
2
3
4
POST abc/_doc
{
"a": 2
}
返回:
1
2
3
4
5
6
7
8
{
"error": {
"type": "index_closed_exception",
"reason": "closed",
"index": "abc"
},
"status": 400
}
✳️ 批量关闭索引(支持通配符)
1
POST ab*,test/_close
返回结果:
1
2
3
4
5
6
7
8
9
{
"acknowledged": true,
"shards_acknowledged": true,
"indices": {
"test": { "closed": true },
"abd": { "closed": true },
"abc": { "closed": true }
}
}
确认索引状态:
1
GET _cat/indices?v
🔓 重新打开索引
当需要重新启用这些索引时:
1
POST */_open
返回:
1
2
3
4
{
"acknowledged": true,
"shards_acknowledged": true
}
⚙️ 禁止关闭索引的集群配置
有些场景中(如运营平台防止误操作),管理员可能会禁止索引关闭操作。设置如下:
1
2
3
4
5
6
PUT _cluster/settings
{
"persistent": {
"cluster.indices.close.enable": false
}
}
返回结果表示设置已生效:
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"acknowledged": true,
"persistent": {
"cluster": {
"indices": {
"close": {
"enable": "false"
}
}
}
},
"transient": {}
}
🧯 禁止后关闭索引会报错
再次尝试关闭索引时,将返回如下错误信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"error": {
"root_cause": [
{
"type": "illegal_state_exception",
"reason": "closing indices is disabled - set [cluster.indices.close.enable: true] to enable it. NOTE: closed indices still consume a significant amount of diskspace"
}
],
"type": "illegal_state_exception",
"reason": "closing indices is disabled - set [cluster.indices.close.enable: true] to enable it. NOTE: closed indices still consume a significant amount of diskspace"
},
"status": 500
}
🔍 如何确认关闭被禁用?
执行:
1
GET _cluster/settings
结果会包含:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"persistent": {
"cluster": {
"indices": {
"close": {
"enable": "false"
}
}
},
"index_state_management": {
"template_migration": {
"control": "-1"
}
},
"rollup": {
"search": {
"enabled": "true"
},
"hours_before": "24"
}
},
"transient": {}
}
✅ 总结
操作
是否支持
条件
POST //_close
✅ 默认支持
除非设置 cluster.indices.close.enable: false
POST //_open
✅ 总是支持
无需额外开启
POST ab*/_close
✅ 支持批量关闭
同上
查看关闭限制配置
GET _cluster/settings?include_defaults=true
关闭索引适用于资源控制、调试排查等场景,但要注意:关闭索引仍会占用磁盘空间,不会释放存储,仅仅是节省内存和 CPU 资源。
---
## 懒猫微服实战入门(十五):大疆Pocket3 素材导入懒猫网盘
- URL: https://blog.no-claw.com/posts/50f8f5d2/
- 日期: 2025-06-23
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服
日常使用 Pocket3 拍摄视频,就是共享比较麻烦,不然就得每个手机安装一个 app,然后再导出。突发奇想可以把素材传到懒猫网盘中,这样就解决了这个痛点。
大疆的机器每次连接都要走这个流程,不得不说真的很麻烦。这个 Wi-Fi 的记忆功能比较鸡肋。
连接之后需要右上角先把视频下载到手机本地,然后点击分享。
这里可以选各种软件,比如 airdrop,微信,邮件。我这里选懒猫微服,点击之后就会跳转到懒猫网盘。
第一次传输我发现速度慢的离谱。询问了售后才发现 ios 会默认在 wifi 网络不好时走流量的。
因为大疆的 pocket3 传输需要连接相机的 Wi-Fi。所以手机是整个一断网额的状态。这流量也不快。
偷偷跑流量是手机和运行商的传统了,那么在蜂窝网络里给他关掉。关掉无线局域网助理。
关了之后,再重复上边的操作,就发现懒猫网盘打不开了,嗯 这就是预期的行为了,不会偷偷的用网了。
于是询问大疆额售后能不能让机器连接家里的网,得到的回答是不行。只能手动切换 Wi-Fi。
换了网之后再传输,这个速度就舒服多了,虽然没跑满千兆,但是也能够看了。
然后把上一步的视频文件夹共享出来,以后把素材都发到这个文件夹里。再共享给其他的懒猫用户,就很方便了。
两个很想吐槽的地方
大疆 pocket3 不能直接连家里 Wi-Fi,传文件到网盘必须换网
Apple 的 lighting 原生不支持 OTG,需要买转接头。
---
## 懒猫微服开发篇(零):上架应用需要哪些知识
- URL: https://blog.no-claw.com/posts/c7acbc13/
- 日期: 2025-06-22
- 分类: 懒猫微服 / 开发
- 标签: 开发, 懒猫微服
懒猫微服的可玩性在于可以让开发人员放开手脚来做一些事情,等于是提供了一个可靠的基础设施。那么理所当然我们可以把开源的知识应用到上面,比如开发或者移植应用,或者干脆部署一些好玩的东西。这在传统 NAS 上实现起来很困难,甚至都没有包管理工具。
我们看一看开发懒猫应用,需要什么样的知识?
那么,开发懒猫微服的应用需要掌握哪些技能呢?
NPM
懒猫微服的 CLI 本质上是一个通过 NPM 全局安装的工具包,因此掌握一些基本的 NPM 使用方法是必要的。
1
npm install -g @lazycatcloud/lzc-cli
这个工具是用 JavaScript 编写的,但如果你只是为了使用而非开发,那么并不需要掌握这门语言。当然,你也可以选择使用 pnpm 或 yarn 作为包管理工具,或者通过 NVM 来创建 Node.js 虚拟环境。
如果在 macOS/Linux 上遇到了权限不足的问题,其实不一定要使用 sudo。默认情况下,npm 的全局目录是 /usr/local,普通用户对其没有写权限。比如我们可以看到:
1
2
3
4
5
6
7
8
9
10
11
ll /usr/
total 0
drwxr-xr-x 918 root wheel 29K 6 5 14:05 bin/
drwxr-xr-x 32 root wheel 1.0K 6 5 14:05 lib/
drwxr-xr-x 417 root wheel 13K 6 5 14:05 libexec/
drwxr-xr-x 8 root wheel 256B 6 10 09:31 local/
drwxr-xr-x 230 root wheel 7.2K 6 5 14:05 sbin/
drwxr-xr-x 43 root wheel 1.3K 6 5 14:05 share/
drwxr-xr-x 5 root wheel 160B 6 5 14:05 standalone/
lrwxr-xr-x 1 root wheel 25B 6 5 14:05 X11@ -> ../private/var/select/X11
lrwxr-xr-x 1 root wheel 25B 6 5 14:05 X11R6@ -> ../private/var/select/X11
因此我们可以通过设置 npm 的全局安装目录,规避权限问题。在当前用户目录中创建一个文件夹并添加到环境变量中即可:
1
2
3
4
5
6
7
8
npm config get prefix
/usr/local
mkdir ~/.npm_packages
npm config set prefix ~/.npm_packages/
npm config get prefix
/Users/home/.npm_packages
export PATH=~/.npm-global/bin:$PATH
开发的技能是可选的,如果你只是移植现有的应用的话,那么具备一些 Docker Compose 的知识就足够了,这个我们后面再说。
如果是开发原创 APP 的话,那么无论是 Vue,React,Go,Python 都有用武之地,只要是 Web 的应用能够本地运行或者打包成 Docker 就能上架商店。相信很多开发的小伙伴也会做一些 Devops 的事情,这部分的技能是可以完全迁移过来的。
Linux
很多 NAS 是基于 FreeBSD 或者 Linux 改的,懒猫微服是基于 debian12, 虽然在设计之初是针对非专业玩家。但是后来也开放了 SSH,可以做和其他 Linux 一样的事情,给了 root 用户,所以可以底层操作文件,网络,查看分区,监控,以及系统负载。
所以不是只有树莓派或者自己笔电装机才能学 Linux,懒猫微服的系统重启之后会复原(除了 root 目录和网络设置),所以请随便折腾。
虽然有了一套很完善的图形客户端,但是相信很多专业的玩家还是更加喜欢用传统 Linux 的方式来看待这个微服,我管他叫做拆解系统设计。
举个例子:用 htop 查看负载、用 nmtui 配置网络、用 lsblk 查看磁盘分区、用 systemctl 设置服务自启。深度定制的系统,让我们可以完全无视内核,以及 grub 的这些东西。甚至连 sambda,webdav 这些 server 都不用自己安装。
Docker
Docker 好像对 NAS 玩家是必须的,无论是群晖,威联通。与传统 NAS 不一样的是,懒猫微服集成了三套 docker,分别是系统组件,playground 和应用商店。
playground 就是我们刻板印象的 Docker, 这里叫做pg-docker,所以需要懂一些 Docker 的知识,比如下载,打包,上传,还有数据卷的贡献。甚至包括 Docker- compose 的使用。
应用商店也是基于 Docker 运行的,
上架软件时有两种方式:
直接打包:这个一般用于原创应用或者移植开源无 docker 版本的应用。调试的时候可以使用懒猫内置的 Docker Registry 的 image 进行测试,颇有 VS code remote 的风格。这个调试模式叫做 devshell。
Docker 镜像迁移:一般用于已有的 docker image 的迁移,由于国内出海宽带不足,访问 Docker 经常失败。所以需要使用懒猫提供的 Docker Registry 来做一个国内版本的镜像。然后再做目录的映射。
OIDC
这个稍稍有点跑题,前面的都是传统 Devops 需要的东西。这里的 OIDC 叫做 OpenID Connect,是单点登录实现的一种。传统的认证有基于 cookie 的,或者基于 JWT 的。OIDC 是后者,也是单点登录中最优雅的实现。除了 OIDC 之外,你可能听说过 SAML,Oauth,其实也都是 Single Sign-On 的不同实现,而 Oauth 是和 OpenID Connect 源同一脉,Oauth 的各家实现千差万别,而 OpenID Connect 既统一了规范,解决用户态的问题。换句话说 OAuth 2.0 只是用来授权,颁发的是Access Token,而对于访问者是谁还需要开发人员自己存数据库。OIDC 则是引入了ID Token,这通常是通常是 JWT,所以认证直接请求 IDP 解码就好了。大致是这个流程:
下图是 OIDC 的基本流程:
能够学到哪些知识:
微服内部的官网看起来是根据 OpenResty 改的(个人推断),所以可以来复习一下 nginx 或者 OpenResty 相关的知识
Docker 的使用,容器这几年还是挺火的,移植应用必备,甚至还支持 web VNC。
HTTP 知识:有些情况需要对 http 的请求做特殊的处理,比如加一些自定义 header 或者 cookies
单点登录:微服内置了 OIDC 的认证,应用能够自动帮助我们申请 CLIENT_ID 和 CLIENT_SECRET,简化了和 IDP 打交道的环节。
总结
如果你熟悉 Web 开发、Docker 和基本的 Linux 操作,那么你已经可以快速上手懒猫微服的应用开发。无论是移植开源项目,还是开发原创 App,只要能够在本地运行或打包为 Docker 镜像,就可以顺利上架到应用商店。
懒猫微服不仅仅是一个面向普通用户的 NAS 系统,更是一块为开发者打造的自由试验田 —— 它就是一台稳定可靠的 Debian 云主机,你可以在上面尽情发挥创意与技术。
---
## 懒猫微服进阶心得(九):商店 App 如何接管 Docker 引擎?
- URL: https://blog.no-claw.com/posts/e8e61ce7/
- 日期: 2025-06-21
- 分类: 懒猫微服 / 进阶
- 标签: Docker, 懒猫微服
在之前的内容中,我们提到过懒猫微服采用三套独立的 Docker 环境来隔离系统组件、Playground Docker 与商店 App 的 Docker 实例。那么问题来了:如何让商店中上架的 App 操作 Playground 中的 Docker 引擎?
答案是:通过挂载 docker.sock 文件来实现跨容器控制。
所以我们可以在商店的 APP 中操作 playground docker,其实也就是 Docker 面板或者轻量 Docker 面板做的事情。
为什么不操作其他两个 Docker 引擎?
系统组件 Docker 无需干预,重启之后可以复原。
应用商店有自己的生命周期,也无需干预。
一、在 build.yml 中挂载 Playground 路径
首先,在打包配置 build.yml 中新增 services 字段,用于将宿主机中的 /data/playground 挂载到容器内部:
1
2
3
4
5
6
7
8
9
10
11
12
13
manifest: ./lzc-manifest.yml
pkgout: ./
icon: ./logo.png
services:
containly:
volumes:
- bind:
create_host_path: true
source: /data/playground
target: /lzcapp/run/playground
type: bind
打包后会生成一个名为 compose.override.yml 的文件。请注意:即使你手动创建了 compose.override.yml,也可能无法直接生效,必须通过打包流程自动生成。(此结论基于初步测试)
生成后的 compose.override.yml 内容如下:
1
2
3
4
5
6
7
8
services:
containly:
volumes:
- bind:
create_host_path: true
source: /data/playground
target: /lzcapp/run/playground
type: bind
二、修改 manifest.yml 实现 docker.sock 映射
为了让上架 App 操作 Docker,需要手动编辑 manifest.yml 文件,添加以下内容:
1
2
3
4
5
binds:
- /lzcapp/run/playground/docker.sock:/var/run/docker.sock
environment:
- DOCKGE_STACKS_DIR=/lzcapp/var/stacks
- DOCKER_HOST=unix:///lzcapp/run/playground/docker.sock
这样,容器内的 Docker CLI 或管理面板就可以通过 DOCKER_HOST 环境变量,控制宿主机的 Docker 引擎。
三、完整的 manifest.yml 示例
以下是完整可运行的 manifest.yml 配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
lzc-sdk-version: 0.1
package: xu.deploy.containly
version: 0.0.2
name: Containly
description: >-
A fancy, easy-to-use and reactive self-hosted docker compose.yaml
stack-oriented manager.
license: https://choosealicense.com/licenses/mit/
homepage: https://github.com/cloudsmithy/Containly
author: xu
usage: >-
安装完成后,请重启懒猫微服以启用 Docker。
此应用将接管懒猫微服的独立 Docker 守护进程,可能存在安全风险。在授予容器 privileged
等权限之前,请确保容器是安全的,且不会执行危险操作。为了避免潜在风险,请确保:
1. 您了解容器的行为,并确认它们来自可信的源。
2. 容器中没有运行高危命令,且没有暴露不必要的端口或服务。
建议先查阅懒猫微服开发者手册,了解相关特性和限制,并根据手册中的安全建议配置容器。
application:
subdomain: containly
routes:
- /=http://containly.xu.deploy.containly.lzcapp:5000/
services:
containly:
image: registry.lazycat.cloud/u04123229/cloudsmithy/containly:896f4251373d0ebe
binds:
- /lzcapp/run/playground/docker.sock:/var/run/docker.sock
environment:
- DOCKGE_STACKS_DIR=/lzcapp/var/stacks
- DOCKER_HOST=unix:///lzcapp/run/playground/docker.sock
四、总结
通过挂载 docker.sock 文件和设置 DOCKER_HOST,我们可以让商店上架的 App 控制懒猫微服的 Playground Docker 实例。我用这个功能上架了自己写的 Docker 面板,一起来玩一玩嘛?
---
## MacOS 26 beta 如何恢复启动台
- URL: https://blog.no-claw.com/posts/83eb1399/
- 日期: 2025-06-21
- 分类: 电子产品 / 电脑 / 苹果
- 标签: Apple
苹果最新版的系统使用了年份命名,主要是毛玻璃风格,虽然毛玻璃用了几天慢慢习惯了。
另一个槽点是 apple 把启动台去掉了,改成了 apps,用搜索栏统一搜索。
除了检索应用,也能检索邮件和文件什么的,属于是一键搜索了。
那么我们怎么改原来的启动台呢?执行这两个命令然后重启:
1
2
3
sudo mkdir -p /Library/Preferences/FeatureFlags/Domain
sudo defaults write /Library/Preferences/FeatureFlags/Domain/SpotlightUI.plist SpotlightPlus -dict Enabled -bool false
GPT 解释如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#### 第 1 行:
```bash
sudo mkdir -p /Library/Preferences/FeatureFlags/Domain
```
* 创建一个目录用于配置 **macOS 内部功能标志(FeatureFlags)**。
* `-p` 参数确保中间目录不存在时也一并创建。
#### 第 2 行:
```bash
sudo defaults write /Library/Preferences/FeatureFlags/Domain/SpotlightUI.plist SpotlightPlus -dict Enabled -bool false
```
* 使用 macOS `defaults` 命令写入系统级别的配置文件。
* `SpotlightUI.plist` 是控制 Spotlight(聚焦搜索)的功能标志文件。
* `SpotlightPlus` 是 macOS 14 Sonoma 起的一个实验性 Spotlight 增强功能。
* `Enabled -bool false` 意思是**禁用 Spotlight Plus(新版搜索界面)**。
---
### 🧩 背景用途:
这个配置被用来:
| 场景 | 描述 |
| ------------------ | ---------------------------------------- |
| 💻 macOS Sonoma 用户 | 禁用默认启用的“Spotlight Plus”功能,恢复旧版 Spotlight |
| 🧪 实验功能控制 | macOS 通过隐藏的 feature flag 启用或禁用某些 GUI 功能 |
| ⚙️ 解决兼容问题 | 有些用户觉得新版 Spotlight 太慢或不兼容快捷键脚本,想禁用 |
---
### 🚨 注意事项:
1. 更改后可能需要 **重启系统或注销登录** 才能生效。
2. 如果系统升级,Apple 可能会重置该设置。
3. 这种方法是“非公开支持”的隐藏设置,未来 macOS 可能废弃此路径。
---
如果你想恢复 Spotlight Plus,只需执行:
```bash
sudo defaults delete /Library/Preferences/FeatureFlags/Domain/SpotlightUI.plist SpotlightPlus
```
或者将 `-bool true` 写回去。
---
如你还有其他想隐藏或禁用的 macOS 功能(比如 Siri、Stage Manager、控制中心某模块),我可以帮你查找对应的 `defaults write` 配置。需要吗?
重启之后我们的启动台就回来了:
---
## 懒猫微服排查篇(一):403 Forbidden!记应用商店 pip 使用清华源的报错
- URL: https://blog.no-claw.com/posts/c20c194c/
- 日期: 2025-06-20
- 分类: 懒猫微服 / 排查
- 标签: Python, 懒猫微服
由于自身调试 OIDC 需要,写了一个懒猫 ENV 查看器,然后发到商店里给大家使用/
打包的时候偷了个懒,直接打包的没有用容器,然后其中一位用户就遇到这个问题:
要了下日志,结果是发现访问清华源有问题,被清华源直接返回了 403,这个问题还比较稀奇,毕竟在我的印象里清华源一直都很稳。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
-------------logs:-------------
app-1 | 2025-06-23T14:28:36.813573986Z Start Child Process: [sh -c ./lzcapp/pkg/content/run.sh]
app-1 | 2025-06-23T14:28:36.813587080Z PATH:"/" is served by "exec"://"5005,./lzcapp/pkg/content/run.sh"
app-1 | 2025-06-23T14:28:36.818585835Z fetch https://mirrors.ustc.edu.cn/alpine/v3.20/main/x86_64/APKINDEX.tar.gz
app-1 | 2025-06-23T14:28:36.997192042Z fetch https://mirrors.ustc.edu.cn/alpine/v3.20/community/x86_64/APKINDEX.tar.gz
app-1 | 2025-06-23T14:28:37.251288967Z 2025/06/23 22:28:37 http: proxy error: dial tcp 127.0.0.1:5005: connect: connection refused
app-1 | 2025-06-23T14:28:37.251446934Z 2025/06/23 22:28:37 http: proxy error: dial tcp 127.0.0.1:5005: connect: connection refused
app-1 | 2025-06-23T14:28:37.444530406Z v3.20.6-201-gd57c9d7d1c9 [https://mirrors.ustc.edu.cn/alpine/v3.20/main]
app-1 | 2025-06-23T14:28:37.444552892Z v3.20.6-203-g66ce3a1d0dc [https://mirrors.ustc.edu.cn/alpine/v3.20/community]
app-1 | 2025-06-23T14:28:37.444560548Z OK: 24177 distinct packages available
app-1 | 2025-06-23T14:28:37.732800630Z (1/25) Installing libbz2 (1.0.8-r6)
app-1 | 2025-06-23T14:28:37.834399822Z (2/25) Installing libexpat (2.7.0-r0)
app-1 | 2025-06-23T14:28:37.978333761Z (3/25) Installing libffi (3.4.6-r0)
app-1 | 2025-06-23T14:28:37.996681317Z (4/25) Installing gdbm (1.23-r1)
app-1 | 2025-06-23T14:28:38.023808099Z (5/25) Installing xz-libs (5.6.2-r1)
app-1 | 2025-06-23T14:28:38.059202322Z (6/25) Installing libgcc (13.2.1_git20240309-r1)
app-1 | 2025-06-23T14:28:38.120270779Z (7/25) Installing libstdc++ (13.2.1_git20240309-r1)
app-1 | 2025-06-23T14:28:38.219766620Z (8/25) Installing mpdecimal (4.0.0-r0)
app-1 | 2025-06-23T14:28:38.244870690Z (9/25) Installing ncurses-terminfo-base (6.4_p20240420-r2)
app-1 | 2025-06-23T14:28:38.266752323Z (10/25) Installing libncursesw (6.4_p20240420-r2)
app-1 | 2025-06-23T14:28:38.301269438Z (11/25) Installing libpanelw (6.4_p20240420-r2)
app-1 | 2025-06-23T14:28:38.318329841Z (12/25) Installing readline (8.2.10-r0)
app-1 | 2025-06-23T14:28:38.474049984Z (13/25) Installing sqlite-libs (3.45.3-r2)
app-1 | 2025-06-23T14:28:38.628199528Z (14/25) Installing python3 (3.12.11-r0)
app-1 | 2025-06-23T14:28:39.316803498Z WAIT ./lzcapp/pkg/content/run.sh 127.0.0.1:5005 ERR: dial tcp 127.0.0.1:5005: connect: connection refused
app-1 | 2025-06-23T14:28:39.367766399Z (15/25) Installing python3-pycache-pyc0 (3.12.11-r0)
app-1 | 2025-06-23T14:28:39.753346082Z (16/25) Installing pyc (3.12.11-r0)
app-1 | 2025-06-23T14:28:39.770701744Z (17/25) Installing py3-setuptools-pyc (70.3.0-r0)
app-1 | 2025-06-23T14:28:39.929343816Z (18/25) Installing py3-pip-pyc (24.0-r2)
app-1 | 2025-06-23T14:28:40.207250136Z (19/25) Installing py3-parsing (3.1.2-r1)
app-1 | 2025-06-23T14:28:40.304655931Z (20/25) Installing py3-parsing-pyc (3.1.2-r1)
app-1 | 2025-06-23T14:28:40.317188019Z WAIT ./lzcapp/pkg/content/run.sh 127.0.0.1:5005 ERR: dial tcp 127.0.0.1:5005: connect: connection refused
app-1 | 2025-06-23T14:28:40.529573664Z (21/25) Installing py3-packaging-pyc (24.0-r1)
app-1 | 2025-06-23T14:28:40.618998893Z (22/25) Installing python3-pyc (3.12.11-r0)
app-1 | 2025-06-23T14:28:40.653772503Z (23/25) Installing py3-packaging (24.0-r1)
app-1 | 2025-06-23T14:28:40.830130888Z (24/25) Installing py3-setuptools (70.3.0-r0)
app-1 | 2025-06-23T14:28:40.995056226Z (25/25) Installing py3-pip (24.0-r2)
app-1 | 2025-06-23T14:28:41.318293072Z WAIT ./lzcapp/pkg/content/run.sh 127.0.0.1:5005 ERR: dial tcp 127.0.0.1:5005: connect: connection refused
app-1 | 2025-06-23T14:28:41.764687778Z Executing busybox-1.36.1-r29.trigger
app-1 | 2025-06-23T14:28:41.768553945Z OK: 75 MiB in 42 packages
app-1 | 2025-06-23T14:28:42.318842615Z WAIT ./lzcapp/pkg/content/run.sh 127.0.0.1:5005 ERR: dial tcp 127.0.0.1:5005: connect: connection refused
app-1 | 2025-06-23T14:28:42.994899875Z Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple
app-1 | 2025-06-23T14:28:43.192401924Z Collecting Flask (from -r ./requirements.txt (line 1))
app-1 | 2025-06-23T14:28:43.219653783Z ERROR: HTTP error 403 while getting https://pypi.tuna.tsinghua.edu.cn/packages/3d/68/9d4508e893976286d2ead7f8f571314af6c2037af34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl (from https://pypi.tuna.tsinghua.edu.cn/simple/flask/) (requires-python:>=3.9)
app-1 | 2025-06-23T14:28:43.219966823Z ERROR: Could not install requirement Flask from https://pypi.tuna.tsinghua.edu.cn/packages/3d/68/9d4508e893976286d2ead7f8f571314af6c2037af34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl (from -r ./requirements.txt (line 1)) because of HTTP error 403 Client Error: Forbidden for url: https://pypi.tuna.tsinghua.edu.cn/packages/3d/68/9d4508e893976286d2ead7f8f571314af6c2037af34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl for URL https://pypi.tuna.tsinghua.edu.cn/packages/3d/68/9d4508e893976286d2ead7f8f571314af6c2037af34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl (from https://pypi.tuna.tsinghua.edu.cn/simple/flask/) (requires-python:>=3.9)
app-1 | 2025-06-23T14:28:43.319551942Z WAIT ./lzcapp/pkg/content/run.sh 127.0.0.1:5005 ERR: dial tcp 127.0.0.1:5005: connect: connection refused
app-1 | 2025-06-23T14:28:43.578023168Z Traceback (most recent call last):
app-1 | 2025-06-23T14:28:43.578037493Z File "/lzcapp/pkg/content/app.py", line 2, in
app-1 | 2025-06-23T14:28:43.578038656Z from flask import Flask, request, render_template
app-1 | 2025-06-23T14:28:43.578039625Z ModuleNotFoundError: No module named 'flask'
app-1 | 2025-06-23T14:28:43.581234895Z ChildProcess exit: exit status 1
app-1 | 2025-06-23T14:28:44.320074235Z WAIT ./lzcapp/pkg/content/run.sh 127.0.0.1:5005 ERR: dial tcp 127.0.0.1:5005: connect: connection refused
app-1 | 2025-06-23T14:28:45.320738420Z WAIT ./lzcapp/pkg/content/run.sh 127.0.0.1:5005 ERR: dial tcp 127.0.0.1:5005: connect: connection refused
app-1 | 2025-06-23T14:28:46.321124378Z WAIT ./lzcapp/pkg/content/run.sh 127.0.0.1:5005 ERR: dial tcp 127.0.0.1:5005: connect: connection refused
app-1 | 2025-06-23T14:28:47.322143472Z WAIT ./lzcapp/pkg/content/run.sh 127.0.0.1:5005 ERR: dial tcp 127.0.0.1:5005: connect: connection refused
app-1 | 2025-06-23T14:28:48.322911993Z WAIT ./lzcapp/pkg/content/run.sh 127.0.0.1:5005 ERR: dial tcp 127.0.0.1:5005: connect: connection refused
app-1 | 2025-06-23T14:28:49.323128356Z WAIT ./lzcapp/pkg/content/run.sh 127.0.0.1:5005 ERR: dial tcp 127.0.0.1:5005: connect: connection refused
app-1 | 2025-06-23T14:28:50.323783719Z WAIT ./lzcapp/pkg/content/run.sh 127.0.0.1:5005 ERR: dial tcp 127.0.0.1:5005: connect: connection refused
app-1 | 2025-06-23T14:28:51.323982245Z WAIT ./lzcapp/pkg/content/run.sh 127.0.0.1:5005 ERR: dial tcp 127.0.0.1:5005: connect: connection refused
app-1 | 2025-06-23T14:28:52.324934294Z WAIT ./lzcapp/pkg/content/run.sh 127.0.0.1:5005 ERR: dial tcp 127.0.0.1:5005: connect: connection refused
app-1 | 2025-06-23T14:28:53.325225164Z WAIT ./lzcapp/pkg/content/run.sh 127.0.0.1:5005 ERR: dial tcp 127.0.0.1:5005: connect: connection refused
app-1 | 2025-06-23T14:28:54.325889637Z WAIT ./lzcapp/pkg/content/run.sh 127.0.0.1:5005 ERR: dial tcp 127.0.0.1:5005: connect: connection refused
app-1 | 2025-06-23T14:28:55.326592221Z WAIT ./lzcapp/pkg/content/run.sh 127.0.0.1:5005 ERR: dial tcp 127.0.0.1:5005: connect: connection refused
app-1 | 2025-06-23T14:28:56.326903845Z WAIT ./lzcapp/pkg/content/run.sh 127.0.0.1:5005 ERR: dial tcp 127.0.0.1:5005: connect: connection refused
app-1 | 2025-06-23T14:28:57.327197993Z WAIT ./lzcapp/pkg/content/run.sh 127.0.0.1:5005 ERR: dial tcp 127.0.0.1:5005: connect: connection refused
app-1 | 2025-06-23T14:28:58.327666585Z WAIT ./lzcapp/pkg/content/run.sh 127.0.0.1:5005 ERR: dial tcp 127.0.0.1:5005: connect: connection refused
GPT 查询了下,可能就是 IP 给限制了。
打开清华的 Pypi 的页面,看到这位的 IP 确实被清华拦截了。
为了防止这个情况,有两个办法:
直接使用 Docker 做好镜像,这样就无关软件源
可以同时设置其他 pypi 源:
1
pip config set global.extra-index-url "https://mirrors.aliyun.com/pypi/simple/ https://repo.huaweicloud.com/repository/pypi/simple/ https://mirrors.cloud.tencent.com/pypi/simple/"
然后可以使用 pip config list 查看,能看到我这个是走了腾讯云的:
1
2
3
4
5
6
7
8
9
10
11
pip install pandas
Looking in indexes: https://pypi.org/simple, https://mirrors.aliyun.com/pypi/simple/, https://repo.huaweicloud.com/repository/pypi/simple/, https://mirrors.cloud.tencent.com/pypi/simple/
Collecting pandas
Downloading https://mirrors.cloud.tencent.com/pypi/packages/9f/cc/ae8ea3b800757a70c9fdccc68b67dc0280a6e814efcf74e4211fd5dea1ca/pandas-2.3.0-cp312-cp312-macosx_11_0_arm64.whl (10.7 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 10.7/10.7 MB 3.6 MB/s eta 0:00:00
Requirement already satisfied: numpy>=1.26.0 in /opt/miniconda3/lib/python3.12/site-packages (from pandas) (1.26.4)
Requirement already satisfied: python-dateutil>=2.8.2 in /opt/miniconda3/lib/python3.12/site-packages (from pandas) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /opt/miniconda3/lib/python3.12/site-packages (from pandas) (2025.1)
Requirement already satisfied: tzdata>=2022.7 in /opt/miniconda3/lib/python3.12/site-packages (from pandas) (2025.1)
Requirement already satisfied: six>=1.5 in /opt/miniconda3/lib/python3.12/site-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)
Installing collected packages: pandas
---
## 懒猫微服实战入门(十四):不登录客户端,如何用 Samba 挂载懒猫微服网盘?
- URL: https://blog.no-claw.com/posts/86ecba8b/
- 日期: 2025-06-20
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服, Samba
懒猫微服网盘自带自动挂载 Samba 功能,但如果不登录客户端,也可以手动通过微服的私有地址来挂载,实现访问和高速传输。
由于懒猫网盘采用多租户架构,每位用户的数据是隔离的。因此,访问路径通常为:
1
smb:///<用户名>
🧩 步骤一:开启内网访问服务
在开始挂载前,请确保你已在懒猫微服后台开启了“内网访问服务”,否则 SMB 连接会被拒绝。
💡 回忆一波上机考试的方式
以前在上机考试时,老师会将题目放在服务器共享目录中,我们用 Win + R 输入 \\IP地址 来下载资料,当时还觉得这操作很高端。
解释:
这是通过 Win + R 快捷键打开“运行”窗口,输入 \\ 快速访问局域网 SMB 共享目录。通常用于临时打开文件夹,不做映射。
🐧 macOS/Linux 挂载方式(CLI)
在 Linux 或 macOS 上,可以直接用以下地址挂载:
1
smb:///Download
或者通过 mount.cifs:
1
sudo mount -t cifs //192.168.1.100/your-username /mnt/share -o user=your-username,password=your-password
🪟 Windows 上手动挂载 SMB 网盘
在 Windows 中,需要通过图形界面手动挂载为本地磁盘,操作如下:
第一步:右键“此电脑” → 映射网络驱动器
解释:
在这里你可以:
选择盘符(建议使用末尾的 Z:, Y:, X: 等);
输入共享路径(例如 \\192.168.1.100\your-username);
可勾选“使用其他凭据连接”等选项。
第二步:确认挂载路径并验证身份
系统将提示输入用户名和密码:
第三步:挂载成功,查看 Z 盘内容
成功后,可以在“此电脑”中看到挂载好的 SMB 网络盘:
你可以像操作本地硬盘一样打开、拖拽、复制文件。
打开挂载目录后的界面如下:
🚀 实测传输速度:约 500MB/s
我测试了一下,将文件从 SMB 网盘拖入 PVE 虚拟机,传输速度稳定在 500MB/s,表现不错。
---
## Amazon OpenSearch Service 现在支持 JSON Web Token(JWT)身份验证和授权
- URL: https://blog.no-claw.com/posts/330e574/
- 日期: 2025-06-19
- 分类: 软件 / AWS
- 标签: AWS
最近,Amazon OpenSearch 推出了一个新功能,支持 JWT 认证和授权。虽然这个功能在开源的 OpenSearch 中早已存在,但在托管的 Amazon OpenSearch 中的实现一直不够理想。
此前的授权方式
控制台登录
内部数据库:使用基本的用户名和密码进行 HTTP 验证。如果切换到其他认证方式(如 IAM 或 SAML),此验证方式将被禁用。
IAM 主用户:实际上是通过 Cognito 进行验证。由于中国区没有用户池,设置为 IAM 作为主用户故无法使用。
SAML 单点登录:与 SAML 身份提供商(如 Azure AD、Auth0)集成。SAML 身份验证仅能在浏览器中访问 OpenSearch Dashboard,开启 SAML 后会禁用基本的 HTTP 验证。
编程方式
对于 SDK 而言,可以通过在 HTTP 请求中携带用户名和密码,或使用 SignV4 携带 IAM 身份信息进行认证。
常见的解决方案包括:
控制台和 SDK 都使用内部数据库的主用户进行基本 HTTP 验证。
控制台使用内部数据库或 SAML 凭证登录 OpenSearch Dashboard,然后在权限认证中给 IAM 身份单独授权访问索引,这样编程方式就可以使用 SignV4 的签名算法访问集群资源。
JWT 与 OIDC
JWT 验证流程
客户端请求:客户端向服务器发出登录请求,提供用户凭证(例如用户名和密码)。
服务器验证凭证:服务器验证用户凭证的有效性。
生成 JWT:如果凭证有效,服务器生成一个包含用户身份信息和其他声明的 JWT,并使用服务器的私钥签名。
返回 JWT:服务器将签名的 JWT 返回给客户端。
客户端存储 JWT:客户端收到 JWT 后,将其存储在本地存储或 cookie 中,以便在后续请求中使用。
携带 JWT 的请求:客户端在每次请求时将 JWT 包含在 HTTP 请求头中(通常是 Authorization: Bearer )。
服务器验证 JWT:服务器接收到请求后,提取并解析 JWT,验证其签名、有效期和其他声明的合法性。
处理请求:如果 JWT 验证通过,服务器处理请求并返回响应;如果验证失败,返回 401 或 403 错误。
OIDC 验证流程
OpenID Connect(OIDC)是在 OAuth 2.0 协议之上构建的一个身份层,用于实现单点登录(SSO)和身份验证。以下是 OIDC 的详细验证流程:
客户端请求身份认证:客户端向身份提供者(IdP)发送身份认证请求,包含 client_id、redirect_uri、scope、response_type 和 state 参数。
用户身份验证:身份提供者显示登录界面,用户输入凭证进行身份验证。
同意授权:用户登录成功后,身份提供者可能会显示同意授权页面。
返回授权码:用户同意授权后,身份提供者重定向客户端到 redirect_uri,并附带一个授权码。
交换授权码:客户端使用授权码向身份提供者的 Token 端点发送请求,以交换 access token 和 ID token。
返回令牌:身份提供者验证授权码后,返回 access token 和 ID token。ID token 是一个包含用户身份信息的 JWT。
验证 ID token:客户端接收到 ID token 后,验证其签名、声明合法性和过期时间。
使用令牌:客户端使用 access token 访问受保护资源,并解码 ID token 中的用户身份信息。
OpenSearch 的 JWT 认证授权
2024 年 6 月 19 日 Amazon OpenSearch 在全球区上线了 JWT 认证授权功能,中国区的北京和宁夏区域的此功能在 2024 年 6 月 23 日上线控制台增加了 JWT authentication and authorization 功能,启用此功能需要开启精细访问控制,并导入验证 JWT 有效性的证书。
配置 Auth0
配置 JWT 认证授权的步骤包括在 IDP 中创建 API,并使用 API 获取 JWT。以下是使用 Auth0 生成 JWT 的示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import requests
import json
# 配置
AUTH0_DOMAIN = ".auth0.com"
CLIENT_ID = ""
CLIENT_SECRET = ""
AUDIENCE = "https://auth0-jwt-authorize"
GRANT_TYPE = "client_credentials"
TOKEN_URL = f"https://{AUTH0_DOMAIN}/oauth/token"
OUTPUT_FILE_PATH = 'jwt_token.json'
# 请求负载和头部
payload = {
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"audience": AUDIENCE,
"grant_type": GRANT_TYPE
}
headers = {
"content-type": "application/json"
}
# 发送 POST 请求获取 JWT
response = requests.post(TOKEN_URL, json=payload, headers=headers)
# 处理响应
if response.status_code == 200:
data = response.json()
with open(OUTPUT_FILE_PATH, 'w') as file:
json.dump(data, file, indent=4)
print(f"JWT 已保存到文件: {OUTPUT_FILE_PATH}")
else:
print(f"请求失败,状态码:{response.status_code}")
print(response.text)
我们在 Auth0 中新建一个 API,然后会帮我们生成一个 Application。后续我们会使用这个 Application 的 Client ID 和 Secret ID 以及 Domain 的信息来登录。
也就是说这三个信息确定了一个身份池,然后符合规则的用户可以通过这个身份池来换取 JWT。可以在 Applications-Applications 中看到。
配置好之后,可以通过 Auth0 的 API 来拿到登录后的 JWT,以下是一个官方给的教程可以用来测试功能,当然也可以集成到 APP 中。
auth0 也提供了示例代码供我们测试:
官方提供代码样例可读性不是很高,让我们用用 requests 来改写一下,这个代码会把生成的 JWT 存在一个 Json 文件里面,这样我们就能容易的复制出来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import requests
import json
# 配置
AUTH0_DOMAIN = ".auth0.com"
CLIENT_ID = ""
CLIENT_SECRET = ""
AUDIENCE = "https://auth0-jwt-authorize"
GRANT_TYPE = "client_credentials"
TOKEN_URL = f"https://{AUTH0_DOMAIN}/oauth/token"
OUTPUT_FILE_PATH = 'jwt_token.json'
# 请求负载和头部
payload = {
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"audience": AUDIENCE,
"grant_type": GRANT_TYPE
}
headers = {
"content-type": "application/json"
}
# 发送POST请求获取JWT
response = requests.post(TOKEN_URL, json=payload, headers=headers)
# 处理响应
if response.status_code == 200:
data = response.json()
with open(OUTPUT_FILE_PATH, 'w') as file:
json.dump(data, file, indent=4)
print(f"JWT已保存到文件: {OUTPUT_FILE_PATH}")
else:
print(f"请求失败,状态码:{response.status_code}")
print(response.text)
我们可以看到控制台多出了一个 JWT authentication and authorization 新功能,使用这个功能需要先开启精细访问控制,我们需要在这里需要导入验证 JWT 有效性的证书。
服务端需要填写验证 JWT 的 PEM 证书,那么我们要从 Auth0 的 API 中拿到这个信息。使用如下代码从.well-known/jwks.json 中解析出来需要的证书,然后填写到 OpenSearch 中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import requests
from jwcrypto import jwk
import json
# 配置
JWKS_URI = 'https:///.well-known/jwks.json' # 替换为你的Auth0域名
OUTPUT_DIR = './pem_keys' # 你希望保存PEM公钥的目录
# 创建输出目录
import os
if not os.path.exists(OUTPUT_DIR):
os.makedirs(OUTPUT_DIR)
# 获取JWKS
response = requests.get(JWKS_URI)
jwks = response.json()
# 处理每一个JWK
for index, jwk_key in enumerate(jwks['keys']):
try:
key = jwk.JWK(**jwk_key)
pem = key.export_to_pem(private_key=False, password=None).decode('utf-8')
kid = jwk_key['kid']
# 将PEM格式公钥写入文件
pem_file_path = os.path.join(OUTPUT_DIR, f'{kid}.pem')
with open(pem_file_path, 'w') as pem_file:
pem_file.write(pem)
print(f'PEM格式公钥已保存到文件: {pem_file_path}')
except Exception as e:
print(f'处理公钥时出错: {e}')
配置好后,可以在 OpenSearch 的安全配置中看到 “View JWT details” 信息,验证 JWT 的有效性。通过标准的 JWT 流程使用 Postman 验证时,将 JWT 输入到 Bearer token 中,即可进行验证。
postman 测试 JWT
然后我们按照标准的 JWT 流程进行验证,这里使用 Postman,验证方式使用 Bear token,我们把通过应用程序模拟的 JWT 输入进去。
编程方式访问(Python)
我们也可以使用编程方式来进行访问,其实就是上加上一个’Authorization’: ‘Bearer ‘的请求头。
1
2
3
4
5
6
7
8
9
10
11
12
import requests
url = "OpenSearch URL"
payload = {}
headers = {
'Authorization': 'Bearer '
}
response = requests.request("GET", url, headers=headers, data=payload)
print(response.text)
几个月前,我参与了一些内部功能的审核工作。当时认为,既然系统支持 JWT,那么这应该也意味着支持 OIDC IDP,并且能够解决中国区 Cognito 无法集成的问题。然而,通过测试发现,JWT 仅能在编程方式中使用,控制台仍然需要使用原来的认证方式。也就是说,预想的从控制台跳转到 OIDC IDP 的方式仍然无法实现。期待未来能够实现从控制台无缝跳转到 OIDC IDP 的目标,为用户提供更便捷和安全的使用体验。
参考文档
【1】https://aws.amazon.com/cn/about-aws/whats-new/2024/06/amazon-opensearch-service-jwt-authentication-authorization/
【2】https://www.amazonaws.cn/new/2024/amazon-opensearch-service-supports-json-web-token-authentication-and-authorization/
【3】https://docs.aws.amazon.com/zh_cn/opensearch-service/latest/developerguide/JSON-Web-tokens.html
---
## 半小时学会 Amazon Transit Gateway 跨 VPC 网络互联配置
- URL: https://blog.no-claw.com/posts/b9af2b64/
- 日期: 2025-06-19
- 分类: 网络 / AWS
- 标签: AWS
Amazon Transit Gateway (TGW) 是一个强大的网络连接服务,用于在不同的 VPC(虚拟私有云)之间实现高效互联。本文将指导您如何创建和配置 TGW,以便实现跨账户和跨区域的 VPC 互联。
VPC Peering 的局限性
点对点连接:VPC Peering 是一个点对点的连接,每次只能连接两个 VPC。如果需要连接多个 VPC,需要为每对 VPC 单独设置 Peering 连接,也就是我们常说的不能进行路由的传递,需要打通的 VPC 很多的时候会非常的麻烦。
手动路由配置:每个 VPC Peering 连接都需要手动配置路由表,这在大规模环境下非常繁琐。
TGW 的优势
集中式管理:TGW 作为一个中央枢纽,允许多个 VPC 和本地网络通过单个网关相互连接,简化了网络架构和管理。
自动路由传播:TGW 支持自动路由传播,简化了路由配置,减少了人为错误的风险。
跨账户和跨区域支持:TGW 支持跨多个亚马逊云科技账户和跨区域的连接,提供更大的灵活性和扩展性。
总结下来说,TGW 就是是一个中转网关,使用时候需要在需要打通的 VPC 内创建一个挂载点,TGW 会管理一张路由表来决定流量的转发到对应的挂载点上。本质上是 EC2 的请求路由到 TGW,然后在查询 TGW 的路由表来再来决定下一跳,所以需要同时修改 VPC 内子网的路由表和 TGW 的路由表。
TGW 的网络拓扑图如下:
1. 创建 TGW
登录到亚马逊云科技管理控制台,导航到”VPC”服务。
在左侧菜单中选择”Transit Gateways”,点击”Create Transit Gateway”。
填写 TGW 名称和描述,配置 DNS 支持等选项。
根据要求创建 TGW,如果不需要和本地网络打通,这里填写名称和描述就好。
建议开启以下三个选项:
DNS support:开启打通 VPC 的 DNS 支持,这个 DNS support 无法解析对端的私有 R53 记录,还需要使用 Resolver 才行 [^1]
Default route table association:自动创建一个路由表并且关联这个 TGW
Default route table propagation:自动路由表自动传播,这样每次更新的时候就不用手动管理路由。
2. 在每个 VPC 新建挂载点
在 TGW 创建完成后,导航到”Transit Gateway Attachments”。
点击”Create Transit Gateway Attachment”,选择目标 VPC 并配置相关选项。
创建挂载点需要选择关联的 TGW 以及挂载点的 Type,除了 VPC 之外还有 peering,DX 类型的可供选择。
同样这里也要开启对 DNS 的支持,另外关于 Appliance Mode support,如果这个功能开启的话,流量只能在相同的可用区进行转发,这个功能开启需要慎重考虑。
3. 设置 TGW 路由
手动新建 TGW 的路由表并且关联到一个 TGW,如果前面开启了 Default route table association 和 Default route table propagation 不再需要此步骤。
需要在 Routes 部分手动添加路由规则
4. 设置子网路由
为每个 VPC 配置路由表,添加到 TGW 的路由。确保启用路由传播,使 VPC 可以通过 TGW 相互通信。
和 peering 一一样,需要把对应的流量指到对端,这里 10.1.0.0/16 的流量到 TGW。
5. 跨账户 RAM 分享,对端 RAM 接收
如果需要跨账户打通网络,那么需要用到 TGW 的 share 功能,其实就是使用 RAM 进行资源共享。
如果需要跨账户共享 TGW,使用 AWS Resource Access Manager (RAM)。
在 RAM 控制台中创建资源共享并邀请其他 AWS 账户。
对方也是需要在 RAM 里进行确认,并且接收方不能二次 share 此 TGW。
拓展阅读:
Building a global network using Amazon Transit Gateway Inter-Region peering
https://aws.amazon.com/cn/blogs/networking-and-content-delivery/building-a-global-network-using-aws-transit-gateway-inter-region-peering/
Amazon Transit Gateway now supports Inter-Region Peering
https://aws.amazon.com/about-aws/whats-new/2019/12/aws-transit-gateway-supports-inter-region-peering/
Transit Gateway inter-Region peering
https://docs.aws.amazon.com/solutions/latest/network-orchestration-aws-transit-gateway/transit-gateway-inter-region-peering.html
Amazon Transit Gateway - Amazon Virtual Private Cloud Connectivity Options
https://docs.aws.amazon.com/whitepapers/latest/aws-vpc-connectivity-options/aws-transit-gateway.html
Centralized DNS management of hybrid cloud with Amazon Route 53 and AWS Transit Gateway
https://aws.amazon.com/cn/blogs/networking-and-content-delivery/centralized-dns-management-of-hybrid-cloud-with-amazon-route-53-and-aws-transit-gateway/
通过这些文档,可以全面了解 TGW 在跨区域连接中的显著优势,确保在大规模和复杂网络环境中的高效、安全和可扩展性。
---
## 基于 AWS IAM Identity Center 的 SAML 配置,实现单点登录阿里云
- URL: https://blog.no-claw.com/posts/511c83d9/
- 日期: 2025-06-19
- 分类: 软件 / AWS
- 标签: AWS
在企业多云环境日益普及的今天,很多组织不仅使用 AWS(亚马逊云科技) 作为主要的计算与存储平台,同时也会使用 阿里云 来满足本地合规、地域性需求或价格优势。如何在多个云平台之间实现 统一身份认证,避免用户维护多个账号与密码,已经成为企业安全与运维中的核心问题。
AWS IAM Identity Center(身份中心,原 AWS SSO)作为 AWS 官方提供的集中式身份认证与访问管理服务,可以作为企业的 主身份提供商(IdP)。通过 SAML 2.0 协议,它能够将认证结果传递给其他云服务商(如阿里云),让用户在 AWS 完成一次身份验证后,直接进入阿里云控制台,而无需再次登录。这就是所谓的 跨云单点登录(Single Sign-On, SSO)。
本文将结合详细步骤与截图,完整演示如何配置 AWS IAM Identity Center 与阿里云 RAM 的 SAML 对接。
在 AWS IAM Identity Center 新建用户
在正式配置 SAML 对接之前,我们需要先在 AWS IAM Identity Center 中创建用户。
登录 AWS 控制台,进入 IAM Identity Center 页面。
选择 Users → Add user。
填写 用户名、密码、姓名和邮箱。
用户名:用于登录 AWS SSO 的唯一标识。
姓名:通常填写用户的真实姓名,方便后续在阿里云匹配用户。
邮箱:AWS 会将登录邀请邮件发送到该邮箱。
密码:可以由管理员生成初始密码,也可以让用户自行设置。
创建应用并选择 SAML
在用户创建完成后,需要在 AWS IAM Identity Center 中配置一个 应用(Application),它代表与阿里云之间的对接关系。
操作步骤:
打开 Applications → Add application。
在应用类型中选择 SAML 2.0。
将此应用与前面创建的用户或用户组进行绑定。
📌 说明:
应用相当于一个桥梁,AWS 通过它来生成 SAML 断言。
如果未来还要接入其他云服务(如 Salesforce、Office 365),也需要新建对应应用。
建议统一命名规范,例如:AlibabaCloud-SAML。
OAuth 与 SAML 的区别
在配置过程中,你可能注意到 AWS 提供了多种协议选项,其中最常见的就是 OAuth 2.0 与 SAML 2.0。
📌 区别解析:
对比项
SAML
OAuth
核心用途
单点登录(SSO),跨云、跨域身份认证
应用授权,第三方应用获取资源
数据格式
XML
JSON
常见场景
企业员工访问云平台、内部系统
微信/Google 登录第三方应用
安全特性
强调身份认证 + 授权
强调令牌授权,不负责身份本身
适合对象
企业 IT 管理,多云环境
C 端互联网应用
下载 AWS IAM Identity Center 的 SAML 元数据 XML
在创建好应用后,AWS 会为我们生成一个 元数据 XML 文件,其中包含:
AWS IAM Identity Center 的 端点 URL;
公钥证书信息;
支持的协议与绑定方式。
📌 操作方法:
在应用详情页找到 IAM SSO URL。
点击 Download metadata XML。
保存到本地,稍后需要上传到阿里云。
⚠️ 注意事项:
下载后请勿修改 XML 文件内容,否则会导致签名校验失败。
建议妥善保存,并在企业内部文档中记录文件版本。
如果更换证书,需要重新下载并更新到阿里云。
在阿里云导入身份提供商
接下来,切换到 阿里云 RAM 控制台,新建一个 身份提供商(Identity Provider)。
📌 步骤:
进入 访问控制(RAM) → 身份提供商。
选择 SAML 类型。
上传刚才下载的 AWS XML 文件。
在阿里云中,也提供了一个默认的 SAML 元数据地址:
👉 https://signin.aliyun.com/saml-role/sp-metadata.xml
但在本场景下,我们需要导入 AWS 提供的 XML,因为 AWS 是 IdP,阿里云是 SP。
为应用分配用户或用户组
在 AWS IAM Identity Center 应用配置页面,需要把实际用户分配到该应用。
📌 操作细节:
在应用右下角点击 Add user or group。
搜索用户,建议使用 姓名搜索 而不是用户名。
⚠️ 注意事项:
用户名可能无法正确匹配,使用姓名更稳定。
如果分配的是用户组,那么组内所有用户都能通过 SSO 登录阿里云。
建议按 部门 / 职能 建立用户组,例如:DevOps-Team、Finance-Team。
这个应用程序的属性映射如下:
1
2
3
[https://www.aliyun.com/SAML-Role/Attributes/RoleSessionName](https://www.aliyun.com/SAML-Role/Attributes/RoleSessionName) awssso(可以自定义)
[https://www.aliyun.com/SAML-Role/Attributes/Role](https://www.aliyun.com/SAML-Role/Attributes/Role) acs:ram::1647543622349991:role/iamssorole,acs:ram::1647543622349991:saml-provider/iamsso(role, provider的arn)
在阿里云创建角色并建立信任关系
阿里云侧还需要配置一个 角色(Role),与身份提供商进行绑定。
角色配置完成后, 信任策略如下,例如:
在 AWS 上可以看到我们新建的应用程序。
📌 说明:
role/iamssorole 表示阿里云角色。
saml-provider/iamsso 表示 AWS IAM Identity Center 提供的身份。
两者形成绑定关系后,用户通过 AWS SSO 登录时即可扮演此角色。
👉 可配置的属性:
RoleSessionName:会话名称,通常可设置为 awssso 或自定义值。
Role:指定的阿里云角色 ARN。
https://www.aliyun.com/SAML-Role/Attributes/RoleSessionName
https://www.aliyun.com/SAML-Role/Attributes/Role
在配置过程中,阿里云官方也提供了一些参考文档,例如:
https://help.aliyun.com/zh/ram/user-guide/implement-role-based-sso-from-ad-fs
虽然文档以 AD FS 为例,但本质上对接 AWS IAM Identity Center 的原理是一样的。
验证 IAM SSO 登录流程
配置完成后,可以在 AWS IAM Identity Center 的 SSO 页面进行验证。
在 Dashboard 中,点击分配好的 “Alibaba Cloud” 应用:
此时用户会自动跳转到阿里云控制台,无需再次输入用户名和密码:
最佳实践与经验总结
最小权限原则
在阿里云角色策略中,只授予用户所需的最低权限。
分组管理
在 AWS IAM Identity Center 中按部门建组,再映射到阿里云角色。
MFA 多因子认证
在 AWS 侧启用 MFA,提高整体安全性。
跨云审计
结合 AWS CloudTrail 与阿里云 ActionTrail,实现跨平台日志追踪。
会话管理
设置合理的会话过期时间,防止用户长时间保持登录状态。
总结
通过本文的配置流程,我们实现了 AWS IAM Identity Center(IdP) → 阿里云 RAM(SP) 的单点登录:
用户在 AWS 中完成身份认证;
AWS 生成 SAML 断言并传递给阿里云;
阿里云验证后授予对应角色权限;
用户一键跳转进入阿里云控制台。
---
## 组队参加 Amazon Q Idea 1000,我们的作品上了 AWS 峰会
- URL: https://blog.no-claw.com/posts/ac23bcf7/
- 日期: 2025-06-19
- 分类: 软件 / AWS
- 标签: AWS
最近生成式 AI 的比赛很多,也报名了 AWS Idea1000 的比赛,作品登录上了 AWS 的峰会。
我们的产品名称是“拍立懂”,团队名称是 “凌晨三点的夜”。主要是拍照识别商品成分、品牌资质,分析价格合理性,为老年人提供购买决策建议;通过实时语音聊天交互,亲人语音陪伴老年人购物,满足空巢老人的情感空缺。
我们的项目团队汇聚了文化创意创业者、资深互联网产品经理、互联网技术博主与 AI 前端工程师等多元背景,形成从商业策略到产品落地的完整闭环。团队成员对创新技术和用户价值怀有共同的热情,彼此协作、优势互补,致力于在商业模式和技术实现上持续突破,为项目注入持久动力。
说人话版本:
“拍立懂”首页:先定位、再拍照、还可语音,一站式搞定逛超市!
进入小程序,系统会自动识别你所在的门店,保证每一次推荐都“本地有货”。
对准商品“拍一拍”,AI 秒识品牌与规格,为你生成成分/营养解析;
打开语音助手,直接问“这款油健康吗?”,即时语音作答;
想逛逛热卖?下拉切换「日用食品 / 零食饮料 / 时令食材」,AI 列出今日在售优质清单。
拍照 + 语音 + 实时库存,让你逛超市不再纠结,用 AI 把复杂信息说得清清楚楚。
架构图如下:
域名与流量入口
Route 53 购买域名以及负责域名解析
ALB 负载均衡 & HTTPS 卸载
应用与模型服务
Next.js (前端+SSR)
Flask (语音识别、图像识别、流式对题) + FastAPI (OpenAI realtime 代理)
数据与存储
S3 存音频/图片
Milvus (Zilliz Cloud on GCP) 向量检索做 RAG
多模态交互
OpenAI Vision + Realtime 实时对话
环境 & CI/CD
Lightsail 做 POC,懒猫微服异地组网调试
GitHub Actions 一件打包到 Dockerhub
Docker-compose 一键部署
这个是最初的设计。
门店定位(顶部)
自动显示 _“上海市黄浦区马当路富民超市”_,说明系统已精准锁定当前购物地点,为后续推荐做铺垫。
商品识别卡片(居中大框)
灰阶渐变 + 相机图标,文案 _“对准商品 拍一拍”_。
点击即可拍照识别商品,进入智能解析流程,是整套体验的“入口 1”。
语音购物助手按钮(绿色条形)
显眼的绿色按钮 _“打开语音购物助手”_。
用户只需轻触,便可与 AI 语音对话,实现“入口 2”。
AI 推荐栏目(下方列表)
栏目标题 _“AI 帮你聪明买”_,右侧分类下拉框默认 _“日用食品”_。
下拉可切换 “零食饮料 / 时令食材” 等,列表随之刷新“今日在售”优选商品。
拍一下,AI 帮你把配料表翻译成人话!
识别完成后,只需点开「成分分析」,AI 会:
精准列出主要成分、配料比例;
提醒潜在风险(如高糖、高钠或人工添加);
结合膳食指南,给出健康建议。
如果想进一步了解热量、矿物质等信息,切换到「营养价值」即可;想知道同类好物怎么选?点「选购建议」一键获得。
拍立懂,让任何瓶瓶罐罐都不再是“天书”,分分钟看懂喝得放心!
“拍一下成分表,让 AI 帮你读懂配料、评估健康影响,真正做到买得安心、喝得放心。”
“坚果零食好不好?拍立懂 3 秒告诉你!”
拍照上传后,AI 自动识别到「烤坚果夹片 145 g × 24 片」,并细致解析:
坚果种类、外层配料、甜味剂 / 添加剂 全部列出;
逐项点评对心血管、体重管理、过敏风险的影响;
支持一键切换到「营养价值」查看热量、蛋白质等详细数据,或点「选购建议」获取更健康替代品。
拍立懂 —— 把复杂配料表翻译成人话,让零食的健康度一眼可见!
一句话上手语音购物助手
点一下麦克风,不用输入、不用切页面,直接对 AI 说:“这款牛奶适合减肥吗?”——拍立懂立刻回答,让逛超市像跟朋友聊天一样简单。
极速连接:3 秒内完成网络 & 麦克风检测
实时反馈:对话状态一目了然,杜绝“说了没听见”
智能引导:AI 主动提问,帮你快速聚焦购物痛点
语音 + AI,让信息检索更高效,让购物决策更轻松!
“无需打字,长按语音键即可提问。拍立懂即刻为你解惑,让购物沟通更流畅。”
商业计划书
https://edwqcun7v5.feishu.cn/docx/VBtWdFMB3omZxhxT2FGco73MnIe
产品演示文档
https://edwqcun7v5.feishu.cn/docx/RNvldJCQVoU1nixqHjrcZ2TNnfc
⏰ 凌晨三点的夜 还在码字、调参,但有热情就不困。
拍立懂,让爸妈“拍一拍就懂”,也让我们更懂爸妈。
---
## 懒猫微服入门篇(零):开箱初探,硬件亮相
- URL: https://blog.no-claw.com/posts/4022bac/
- 日期: 2025-06-18
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服, 电脑外设
用了一个半月的懒猫微服,来写一个伪开箱。机甲风格外壳也得值个几百块,这个外壳还支持拆机之后全身水洗。在桌面上确实颜值很高,在各个小主机都是塑料外壳的时代,属于用心良苦了。
这张图是创始人在用户群里发的实拍图:
据群友们聊天说,这台机器已经经过了多个版本迭代,最后定型为现在这个样子。以后不要再用鞋盒了。
系统与配置
通过 neofetch 可以看到,这台机器预装的是 Debian 12,实际系统也做了不少魔改优化。核心配置如下:
CPU:Intel Core i5-1155G7 @ 4.5GHz(4 核心 8 线程),相当于现在主流的 N305 强了很多。
内存:笔记本内存 DDR4,最大可扩展至 32GB,3200MHZ (买的时候可以谈从 16G 升级到 32G)
显卡:Intel Iris Xe Graphics,英特尔最强核显,核显频率最高 1.35GHz,支持 4K 高清输出与 H.265 视频硬解
存储:
系统盘:512GB PCIe 4.0 NVMe,用于运行系统组件和保存根目录文件。
数据盘:两个 2.5 寸 SATA 盘位,可以组 RAID0 或者 RAID1。SSD 或者 HDD 不限。
主板:看上是自己做的,叫做 YENTEK LC2580,启动热键是 F11,进入 BIOS 的按键是 。 没有锁 BIOS,也支持刷成其他的系统。
运行 dmidecode 可见内存状态如下:
1
2
3
4
5
6
7
8
9
10
sudo dmidecode -t memory | grep -e "Size" -e "Form Factor" -e "Locator"
Size: No Module Installed
Form Factor: Unknown
Locator: Controller1-ChannelA-DIMM0
Bank Locator: BANK 0
Size: 32 GB
Form Factor: SODIMM
Locator: Controller0-ChannelA-DIMM0
Bank Locator: BANK 0
...
网络性能
网络配置方面也比较主流:
有线:2.5G 单网口,可惜我的路由器还是千兆不能完全发挥其能力
无线:Intel AX210,支持 WiFi 6 可以跑满千兆。
1
2
3
4
5
(base) lzcbox-029c588e ~ # dmesg | grep -i ethernet
[ 2.946824] Intel(R) 2.5G Ethernet Linux Driver
[ 9.157839] Bluetooth: BNEP (Ethernet Emulation) ver 1.3
(base) lzcbox-029c588e ~ # lspci | grep -i network
04:00.0 Network controller: Intel Corporation Wi-Fi 6 AX210/AX211/AX411 160MHz (rev 1a)
于 WIFI6 来说协商速率一般是 2402Mbps,所以就算达到协商速率的一般的话,也就是差不多千兆,加上很多家用路由也仅仅是千兆,我为了 POE 供电所以牺牲了部分内网带宽,所以没有跑满。如果你有 2.5G 的机器和交换机,那么一定可以跑的很舒服,基本就到机械硬盘的上限了。
CPU 性跑分
跑了几次 geekbench6,能够看到单核心的性能有 1700+,由于测速的同时还在运行很多系统应用,所以实际的数值比这个还要大一些。
后面用了 EndeavourOS KDE 的随身碟,能够看到单核心分数还能再提升。这个 CPU 跑 docker,K8S 甚至虚拟机都没啥问题了。
外部接口
机器背部接口一览,我这边插了一张采集卡,非常实用:
接口方面也非常丰富:
USB:USB 3.0 x3
Type-C:USB 3.2 Type-C x2,支持反冲保护
视频输出:HDMI 2.1 x1,支持 4K 输出、多声道音频
音频接口:3.5mm 耳机口,支持高阻抗耳机
电源接口:DC 5525
整机搭载一枚 17mm 涡轮静音风扇,结合滚珠轴承与自研调速算法,实际体验确实安静,确实没有 3.5 寸硬盘那种炒豆子的声音了。
可以看到 Deepin 的 团队出来做产品的能力还是挺强的,从主板到 OS 的深度定制,技术功底可见一斑。
下面是群友的发的效果图,实物质感确实很棒,欢迎找我下单体验!
---
## 懒猫微服炫技篇(二):使用懒猫微服倒推停电时间
- URL: https://blog.no-claw.com/posts/b6479fd0/
- 日期: 2025-06-17
- 分类: 懒猫微服 / 炫技
- 标签: 懒猫微服
早晨的时候,突然听到饮水机和扫地机器人重启的声音,只有一种可能是电闸重启了。没有人为重启,那就是意外断电了。赶紧 SSH 登录到懒猫微服,第一时间查看 uptime,果然 3 分钟之前重启了。懒猫的 BIOS 有上电自启的功能,所以能够看到确实是停电了。
使用 uptime -s 显示系统最后一次启动的具体时间
然后又看了看群晖,群晖接到了 UPS 上,几乎没受到啥影响。
昨天刚把懒猫从 UPS 上拿下来,因为发现 UPS 有无线干扰。结果第二天就遇到断电,果然是怕啥来啥。
先看看开机时间:
1
journalctl --list-boots # 列出所有启动记录
重点观察:
LAST ENTRY:上次关机时间
FIRST ENTRY:本次启动时间
这两个时间中间如果有一个空档,而没有正常的 shutdown 日志,就极可能是断电。
可以看到从 08:31 到 08:41,系统中断了 10 分钟,符合意外断电并自动重启的特征。
然后看看关机日志,基本都是昨天的手动关机的日志,今天意外断电的日志丢失,也在情理之中。
1
journalctl -b -1 | grep -i "shutdown\|power\|crash\|kern.*panic" # 检查上次会话
如果是正常关机,会有 systemd-shutdown 或服务停止记录;
如果是异常断电,则日志会直接中断,没有“收尾”。
到这基本上可以确定是 08 点 31 断电的了,再拉下系统日志:能看到好好的 UPNP 的日志突然中断。然后 10 分钟后转为开机日志。基本上可以确定是意外断电。
1
journalctl -S "2025-06-17 08:30:00" -U "2025-06-17 08:42:00"
赶紧查下 SMART 信息,还好没啥事。
1
smartctl -a /dev/sda
---
## 懒猫微服进阶心得(八):如何在 ARM 机器上下载并运行X86镜像以及在X86上运行ARM镜像
- URL: https://blog.no-claw.com/posts/86ab1d7c/
- 日期: 2025-06-16
- 分类: 懒猫微服 / 进阶
- 标签: Docker, 懒猫微服
之前写好应用做好镜像想扔到懒猫微服上打包,都是先使用 buildx 打包双架构镜像,push 到 dockerhub 上,然后再用微服的 copy image 转成国内的镜像地址,这过程很麻烦。
因为在 Apple Silicon(如 M1/M2 芯片)设备上,默认运行的是 ARM 架构镜像(linux/arm64)。但有些镜像或依赖只支持 X86(linux/amd64)架构。
本文将介绍如何在 ARM 设备上拉取并运行 X86 镜像,以及如何保存和加载镜像。
🐳 拉取 X86 架构的 Docker 镜像
使用 --platform=amd64 参数即可拉取 X86 架构镜像:
1
docker pull --platform=amd64 nginx:latest
docker pull:从远程仓库拉取镜像
--platform=amd64:显式指定拉取 x86_64 架构的镜像
nginx:latest:镜像名与标签
适用于在 M 系列 Mac 上使用 X86 镜像进行兼容性测试或运行仅支持 x86 的应用。
🔍 验证镜像的架构信息
拉取完成后,可通过以下命令确认镜像架构:
1
docker image inspect nginx:latest --format '{{.Os}}/{{.Architecture}}'
示例输出(成功拉取 X86 架构):
1
linux/amd64
⚠️ 遇到的运行报错分析
执行以下命令尝试运行时:
1
docker run --rm -it --platform=amd64 nginx:latest
可能会出现如下错误:
1
docker: Error response from daemon: image with reference nginx:latest was found but its platform (linux/amd64) does not match the specified platform (darwin/amd64)
📌 错误原因解析:
Docker 镜像是 基于 Linux 内核 的容器运行时,不支持 darwin/amd64 平台。你应显式指定目标平台为:
1
--platform=linux/amd64
✅ 正确命令:
1
docker run --rm -it --platform=linux/amd64 nginx:latest
此时 Docker Desktop 会自动调用 qemu 进行跨架构模拟(无需额外配置),即在 ARM Mac 上模拟运行 X86 容器。
📦 Docker 镜像的保存与加载
Docker 提供 save 和 load 命令,支持将镜像打包导出为文件,便于备份或跨设备迁移。
✅ 1. 保存镜像为 .tar 文件
1
docker save -o nginx-amd64.tar nginx:latest
-o nginx-amd64.tar:导出的文件名
nginx:latest:指定要导出的镜像标签
也可以一次保存多个镜像:
1
docker save -o images.tar nginx:latest redis:alpine
✅ 2. 加载 .tar 镜像文件
使用 SCP 或者 FTP 传到懒猫微服上,使用以下命令导入:
1
docker load -i nginx-amd64.tar
导入成功后镜像将出现在 docker images 列表中。
✅ 3. 跨架构导入运行示例
如果你从懒猫微服上保存了镜像(如 linux/amd64 的 nginx),在 ARM Mac 上可通过以下方式运行:
1
docker run --rm -it --platform=linux/amd64 nginx:latest
✅ 4. 导出为压缩文件(可选)
压缩后更便于传输:
1
docker save nginx:latest | gzip > nginx.tar.gz
解压并导入:
1
gunzip -c nginx.tar.gz | docker load
📝 小结
操作
命令
拉取 X86 镜像
docker pull --platform=amd64 nginx:latest
运行 X86 镜像
docker run --rm -it --platform=linux/amd64 nginx:latest
保存镜像
docker save -o nginx.tar nginx:latest
加载镜像
docker load -i nginx.tar
如果是在 懒猫微服 运行 ARM 镜像呢?
✅ 拉取 ARM 架构镜像(在 X86 主机上)
1
docker pull --platform=linux/arm64 nginx:latest
或简写为:
1
docker pull --platform=arm64 nginx:latest
这会拉取适用于 linux/arm64 的 nginx 镜像(即 ARM 设备如 Raspberry Pi 或 Apple Silicon 可运行的版本)。
✅ 运行 ARM 镜像(在 X86 上)
1
docker run --rm -it --platform=linux/arm64 nginx:latest
Docker Desktop 会通过内置的 QEMU 模拟 ARM 架构运行该容器。
⚠️ 前提是你的 Docker 环境启用了 QEMU 多平台支持(默认大多数 Docker Desktop 安装都已经启用)。
✅ 验证运行中的容器架构
方案一:确认 QEMU 是否已配置(X86 主机想运行 ARM 镜像)
如果你在 Intel / X86 主机上运行 –platform=linux/arm64,需要先启用跨架构支持:
1
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
进入容器执行:
1
uname -m
输出应为:
1
aarch64
说明该容器运行在 ARM 架构上。
如果遇到:
exec /docker-entrypoint.sh: exec format error
意味着:你尝试在一个 与镜像架构不匹配的主机或模拟环境中运行该镜像,导致容器入口脚本无法被执行。
目标架构
--platform 参数
常见用途
X86 (Intel/AMD)
linux/amd64
默认平台,大多数镜像的标准版本
ARM (如 M1/M2/Raspberry Pi)
linux/arm64
在 Apple Silicon 上或嵌入式设备运行
在 X86 上模拟 ARM
--platform=linux/arm64
跨架构测试、兼容性验证
---
## 给群晖安装Iperf3,一键局域网测速
- URL: https://blog.no-claw.com/posts/e70dd7d7/
- 日期: 2025-06-14
- 分类: 电子产品 / 电脑 / NAS / 群晖
- 标签: 家庭网络
群晖默认不带 iperf3,所以需要套件来安装:
套件中心搜索 SynoCli,然后安装 SynoCli Monitor Tools ,里面有常见的监控工具。
然后在群晖上启动服务端:
1
2
3
4
5
name@qun:~$ iperf3 -s
-----------------------------------------------------------
Server listening on 5201 (test #1)
-----------------------------------------------------------
在 Mac 上测速,iperf3 -c 和
这两个命令的差别关键在于 数据传输方向,也就是谁是“发”谁是“收”。
✅ iperf3 -c
含义:客户端主动发数据,服务器接收。
方向:📤 客户端 → 服务器(上传测试)
谁发数据?:客户端
谁测带宽?:客户端测发送速度,服务器测接收速度。
🧪 示例:
1
iperf3 -c 192.168.5.171
表示测试从你这台机器上传数据到服务器的带宽。
✅ iperf3 -c -R
含义:反向测试,客户端建立连接后,让服务器发数据。
方向:📥 服务器 → 客户端(下载测试)
谁发数据?:服务器
谁测带宽?:客户端测接收速度,服务器测发送速度。
🧪 示例:
1
iperf3 -c 192.168.5.171 -R
表示测试从服务器下载数据到你的机器的带宽。
🔁 表格对比总结:
命令
数据方向
谁发送
谁接收
测的是什么带宽
iperf3 -c
客户端 → 服务器
客户端
服务器
上传带宽
iperf3 -c -R
服务器 → 客户端
服务器
客户端
下载带宽
🎯 提示:
二者都只能测试一个方向,不会自动“双向”测试。
如果你想做双向测试,请手动运行这两个命令各一次。
---
## 如何在 AWS EC2 上部署 Isaac Sim
- URL: https://blog.no-claw.com/posts/10d4e175/
- 日期: 2025-06-11
- 分类: 软件 / AWS
- 标签: Isaac
网上已经有一些关于在阿里云和腾讯云上部署 Isaac Sim 的教程,本文将带大家了解如何在 AWS EC2 上部署 NVIDIA Isaac Sim 仿真平台,并以 A10G GPU(g5.2xlarge 实例)为例进行实战操作。
一、环境说明
GPU 类型:A10G(适用于 RTX 渲染)
实例类型:g5.2xlarge
操作系统镜像(AMI):
Deep Learning OSS Nvidia Driver AMI GPU PyTorch 2.7 (Ubuntu 22.04)
👉 该镜像自带 NVIDIA 驱动、CUDA、Docker、nvidia-docker,无需手动安装
二、登录 AWS 控制台并创建实例
打开 AWS 官网,点击右上角登录。
选择 使用 Root 账户登录:
输入 root 邮箱和密码,若首次登录需要绑定 MFA(建议使用 Authenticator App):
进入 AWS 控制台后,选择左侧的 EC2,点击右上角的 “启动实例”。
三、配置 EC2 实例(含 GPU 驱动)
选择操作系统镜像(AMI):
搜索并选择:
1
Deep Learning OSS Nvidia Driver AMI GPU PyTorch 2.7 (Ubuntu 22.04)
自带了 NVIDIA 驱动、nvidia-container-toolkit、Docker 等,无需额外安装。
选择实例类型:g5.2xlarge(带 A10G GPU)
创建密钥对:系统会生成 .pem 格式的密钥,下载后:
1
chmod 400 your-key.pem
网络设置:
选择已有 VPC,或默认网络
确保启用公网 IP 分配
安全组设置:
开放所需端口
如部署 livestream 或远程访问,确保相应端口可用
四、连接实例并确认环境
使用 SSH 登录 EC2 实例:
1
ssh -i your-key.pem ubuntu@
查看基本系统信息(需先安装 neofetch):
1
2
sudo apt update && sudo apt install neofetch -y
neofetch
查看 GPU 驱动是否正常:
1
nvidia-smi
查看是否已安装 nvidia-docker 插件:
1
docker info | grep -i nvidia
五、安装 Isaac Sim(官方容器方式)
接下来按照 NVIDIA 官方文档进行 Isaac Sim 的容器部署即可:
👉 文档链接:
https://docs.isaacsim.omniverse.nvidia.com/4.5.0/installation/install_container.html
如果首次启动卡在 RtPso async compilation 阶段较久(10 分钟左右),这是因为光线追踪 shader 正在编译。只要缓存持久化,之后启动会非常快(1 分钟内)。
✅ 总结
通过 AWS 的 G5 系列实例(搭载 A10G GPU),我们可以方便地在云端部署 Isaac Sim。选用 NVIDIA 官方预装驱动的 AMI,可以省去繁琐的 CUDA 和容器配置。搭配持久化缓存和合理的端口管理,即可稳定高效运行 Isaac Sim 的云端仿真。
---
## 懒猫微服故事篇(二):西湖邂逅后,我手把手教她玩转NAS
- URL: https://blog.no-claw.com/posts/943b4ba6/
- 日期: 2025-06-08
- 分类: 懒猫微服 / 故事
- 标签: 懒猫微服, NAS
去年与她相识于杭州,是在西湖的游船上。参加活动过后,我想在杭州逗留一天,看看被世人称为眼泪的西湖水。没做攻略,匆匆向前台要了手册,然后来到距离最近的码头。磨磨蹭蹭之后总算开船,隔着一堆大爷大妈看着看着她在拍照,也邀请我帮她拍照。于是找她要攻略,一起逛三潭映月,净慈寺,讨论雷峰塔的倒下。
因为苏堤,我们聊到东坡,美食以及宦海沉浮。聊到最爱的粤菜和川菜更是共同的爱好,去成都旅游的时候找他要了攻略,并且约定下次去她的城市旅游给我当导游。
https://appstore.lazycat.cloud/#/shop/detail/cloud.lazycat.shell.files
加了微信一直零零碎碎的聊着。一个周末的早上,她找我帮忙转换视频格式。由于微信的限制无法发送大文件,于是我建议她通过懒猫网盘传给我。
于是开始做思想工作,把数据上传到我的家里的懒猫微服上。这里还是感谢信任和支持,没有认为我这个是一些诈骗盗取的网站。(毕竟曾经在学校讨论代理问题,被文科生当成黑客)
然后开账户,她的 windows 好像没有可以扫码的地方,所以我帮助她注册好,然后发给她。和她自己注册不一样的是,她的设备我登录时候我这边会弹出“安全码”,然后再发给她,这样她就就可以自己处理登录的问题了。我给她开了懒猫相册,清单,网盘和一些好玩的 APP,除了处理这个事情之外,也希望后面也能慢慢用起来其他的功能。
我是一个非常不喜欢 MFA 的人,但是这个二次验证还是能够接受。常规的 MFA 是每次登录都要手动输入二次验证码,而这个相同的设备只需要一次。虽然从系统设计的角度上看二者没有太大的区别,但是还是感觉这个设计,用来节约我们浪费在二次验证上的时间。我们不是牛马,我们是人,我要相信自己的验证。
然后我告诉他把文件上传到网盘上,然后共享整个文件夹给我。在我的不完全测试下只有文件夹才能共享,然后操作完文件之后再做同样的操作共享给她。毕竟对比被共享人而言,这个目录是只读的,所以我们用共享给对方来完成这个操作。
当然中间有个小插曲,就是上传需要等好久。她的是文科生所以对网络一知半解。一开始还以为她家的网速慢,还稍微吐槽了下。
上传完成之后,我于是让她测试下家里的网速。毕竟以前找我修电脑的姑娘,不是电脑配置过时就是,几乎完全的电脑盲。然后测试下来发现她家的网速还算可以,主要是国内的运营商限制比较多。国内是唯一按照 BGP 收费的,加上还要打击 PCDN,所以每家的上传少的可怜,30MBPS 是刚好能玩的水准,转算成实际的速度嘛,还是等等吧。
实际上,她给我的文件竟然有 15 个 G 多。这种大文件如果不是自建 NAS 或者商业方案根本没有传输的办法。QQ 和微信限制大小,就连邮箱也要限速 5 个 G。最早以前,我们用的办法就是,分段压缩成多个压缩包,然后一个个传给对面,对面再用相同的办法解开。十分麻烦并且耗时。而使用传统的 NAS 还要面临不定时封端口的噩梦,装机师傅和客服都解释不清楚,没有地方去问。虽然家里有公网 IP,但是不想每天被这种琐碎的问题困扰,当初购买懒猫微服也是最喜欢他们这个穿透的卖点。内网传统的是永恒的问题,然后是动态域名解析,做好还要加上健康检查,之前休假的时候把家里的机器透传到公网上,然后三五天就被封端口,但是回去看的时候内网怎么都是好的,但公网 telnet 依然有问题,除非重启路由器更换 IP。
现在我不用为了网络穿透的问题烦恼了,不用担心二次验证以及黑客攻击的问题。用拓竹的例子来说吧,只有用懒猫是在玩 NAS,其他的传统硬件都是在折腾。折腾固然好,兴趣价更高。若为自由故,二者皆可抛。
懒猫带给我的,是丰富的硬件资源和社群沟通,以及售后的专业和及时。花钱买省心,剩下抄作业。大抵如此了。
---
## 本地 RAG 实战:用 Easysearch + Ollama SDK 半小时搭建检索增强问答系统
- URL: https://blog.no-claw.com/posts/5a5307d/
- 日期: 2025-06-08
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
✅ 目标:只用两台服务器(或同一台)就跑通 “向量检索 + 本地大模型” 原型
✅ 特点:完全离线、依赖极少、部署脚本即文档
✅ 适合:快速 PoC、内网合规场景、想深挖 RAG 工作机理的开发者
生成式 AI 聊天固然强大,但当问题依赖本地私有知识时,单靠 LLM 参数内的“世界记忆”往往答非所问。RAG(Retrieval-Augmented Generation) 的思路是:
把文档切片 → 向量化 → 入库
用户提问 → 同样向量化 → 检索
将召回片段拼进 prompt,让大模型“带着材料”再回答
多数教程直接用云端 Embedding+OpenAI GPT-4o,但一些团队因隐私、成本或离线环境无法这样做。
本文选用:
EasySearch (= OpenSearch + Elastiknn) 做向量存取
Ollama SDK 连接本地 LLM
Python + requests + ollama 三个依赖即可
1. 系统架构
1
2
3
4
用户问题 ──▶ 嵌入模型 (Ollama) ──▶ EasySearch 向量检索 ──▶ Top-k 片段
▲ │
│ │
LLM (Ollama Chat) ◀── 拼 Prompt + 生成答案 ◀───────────────┘
嵌入模型:nomic-embed-text(768 维,多语言通用)
检索引擎:EasySearch 2.x + Elastiknn knn_dense_float_vector
对话模型:deepseek-r1:7b(轻量,好部署;可换 llama3 / qwen)
2. 环境与依赖
1
2
3
4
import os, json, requests, warnings
from ollama import Client
from requests.packages.urllib3.exceptions import InsecureRequestWarning
warnings.filterwarnings("ignore", category=InsecureRequestWarning)
1
2
3
4
5
6
# Python 依赖
pip install ollama requests
# 拉取模型
ollama pull nomic-embed-text
ollama pull deepseek-r1:7b
3. 代码逐段拆解
3.1 全局配置
1
2
3
4
5
6
7
8
9
10
# ────────────── 配置区 ──────────────
ES_URL = os.getenv("ES_URL", "https://localhost:9200")
ES_AUTH = ("admin", "c59a759f31e901e8d279") # 无认证设为 None
INDEX = "rag_demo"
OLLAMA_HOST = os.getenv("OLLAMA_URL", "http://localhost:11434")
EMBED_MODEL = "nomic-embed-text" # 向量模型
CHAT_MODEL = "deepseek-r1:7b" # 对话模型
TOP_K = 4
NUM_CAND = 200
这段代码只是给脚本提前设定一些“连接参数”与“模型选择”,方便后面统一引用。逐行解释如下:
1
ES_URL = "https://:9200"
ES_URL:EasySearch / OpenSearch 集群的完整地址(含协议与端口)。
是占位符,实际部署时要替换成你的 IP 或域名。
如果你的集群没开 TLS,可写成 http://10.0.0.8:9200。
1
ES_AUTH = ("elastic", "password") # 无认证设为 None
ES_AUTH:连接集群的账号密码元组。
脚本里传给 requests 的 auth= 参数,会自动加 Basic Auth 头。
若集群关闭了安全认证或走内网匿名访问,就把它设成 None。
1
INDEX = "rag_demo"
INDEX:向量索引(或文档索引)的名称。
脚本后面会对该索引做 create / bulk write / search 等操作。
换成别的名字时记得保持一致,例如 "knowledge_base"。
1
OLLAMA_HOST = "http://:11434"
OLLAMA_HOST:本地 Ollama 服务的 HTTP 起始地址。
也是占位符;若脚本与 Ollama 在同一台机器,可写 http://localhost:11434。
端口 11434 是 Ollama 默认 REST 端口。
1
EMBED_MODEL = "nomic-embed-text"
EMBED_MODEL:用于生成文本向量(embeddings)的模型名。
在脚本里会调用 client.embeddings(model=EMBED_MODEL, …)。
替换规则:先执行 ollama pull <模型名>,确保本地已下载。
1
CHAT_MODEL = "deepseek-r1:7b"
CHAT_MODEL:负责最终回答的聊天 / 生成式模型。
脚本会用 client.chat(model=CHAT_MODEL, …) 进行对话。
同理,若想用 llama3:8b-chat、qwen:7b-chat 等,先 ollama pull 再改这里。
3.2 嵌入与对话(Ollama SDK)
client 是连接 Ollama 模型服务的客户端,用来发请求。
session 是访问 Elasticsearch 用的请求会话,能提高网络效率。
用指定的嵌入模型(比如 nomic-embed-text)把文本转成向量,用于相似度搜索。
用指定的聊天模型(比如 deepseek-r1:7b)回答问题,返回回复文本。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# ────────────── 初始化 ──────────────
client = Client(host=OLLAMA_HOST)
session = requests.Session()
# ────────────── Ollama 封装 ──────────────
def embed(text: str) -> list[float]:
"""返回文本向量 (list[float])"""
resp = client.embeddings(model=EMBED_MODEL, prompt=text)
return resp["embedding"]
def chat(prompt: str) -> str:
"""与聊天模型对话(完整回复)"""
resp = client.chat(
model=CHAT_MODEL,
messages=[{"role": "user", "content": prompt}],
# stream=False
)
return resp["message"]["content"]
3.3 建索引(Elastiknn)
在构建基于向量的 RAG(Retrieval-Augmented Generation)系统时,我们首先需要在向量数据库中创建一个支持向量检索的索引。本文使用 EasySearch 作为底层存储,向其中注册一个支持近似向量搜索的索引结构。
create_index 函数的作用是通过 RESTful API 创建一个名为 rag_demo 的索引,并定义字段结构如下:
content:文本内容字段,类型为 text,可用于全文搜索或作为上下文返回。
vec:向量字段,类型为 knn_dense_float_vector,支持高维向量的快速相似度搜索。
配置中使用了 LSH(局部敏感哈希)模型与 cosine 相似度度量,同时设定了近似参数 L 与 k,分别控制候选样本数量和返回结果数。
通过设定 “index.knn”: True,该索引支持使用 k-NN 查询来高效地检索与查询向量最相似的文档。在实际使用中,嵌入模型如 nomic-embed-text 可将输入文本转换为高维向量,存入此索引中,与用户查询语义对齐,实现高效的语义检索能力。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# ---------- ① 创建索引:Elastiknn 映射 ----------
def create_index(dim: int):
mapping = {
"settings": { "index.knn": True },
"mappings": {
"properties": {
"content": { "type": "text" },
"vec": {
"type": "knn_dense_float_vector",
"knn": {
"dims": dim,
"model": "lsh", # 也可 hnsw / exact
"similarity": "cosine",
"L": 99,
"k": 1
}
}
}
}
}
r = session.put(f"{ES_URL}/{INDEX}",
json=mapping, verify=False, auth=ES_AUTH)
if r.status_code not in (200, 201):
if "resource_already_exists_exception" not in r.text:
print("Create index error:\n", r.text)
r.raise_for_status()
3.4 写入文档
以下是对这段 bulk_upload 函数的简明解释,可用于博客正文或技术文档:
在 RAG 系统中,为了支持高效的语义检索,我们需要将原始文本与其对应的向量一起写入向量索引中。bulk_upload 函数正是完成这一任务的核心组件,它使用 Elasticsearch 的 _bulk 接口实现批量写入,显著提高写入效率。
每条记录包含两个部分:
index 元数据,指定目标索引(rag_demo)及文档 _id。
实际文档内容,包括:
content:原始文本内容;
vec:对应的文本向量,必须使用 {"values": [...]} 的对象结构。
向量通过 embed(t) 获得,调用本地部署的 Ollama 模型(如 nomic-embed-text)生成。
所有数据最终编码为 JSON,通过 Content-Type: application/x-ndjson 提交到 /_bulk API 接口,实现一次性批量写入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ---------- ② 批量写入:向量必须包 {"values": …} ----------
def bulk_upload(texts: list[str]):
bulk = []
for i, t in enumerate(texts):
bulk.append({ "index": { "_index": INDEX, "_id": i } })
bulk.append({
"content": t,
"vec": { "values": embed(t) } # ★ 关键:对象格式
})
ndjson = "\n".join(json.dumps(d, ensure_ascii=False) for d in bulk) + "\n"
r = session.post(f"{ES_URL}/_bulk",
data=ndjson.encode("utf-8"),
headers={"Content-Type": "application/x-ndjson"},
verify=False, auth=ES_AUTH)
r.raise_for_status()
3.5 语义检索
这段 search 函数的作用是在 RAG 系统中执行基于向量的语义检索,以下是适合用于博客中的简明解释:
RAG 系统的核心是从向量索引中找到与用户问题最相近的语义片段。search 函数即完成了这个过程,它调用 EasySearch 的向量检索接口,返回最相似的文本内容。
文本向量化:通过 embed(question) 把用户输入的问题转换成向量 qvec。
构造检索请求:
使用 knn_nearest_neighbors 查询
field: 向量字段名(本例中是 "vec");
vec: 查询向量,必须写成 { "values": [...] } 的对象结构;
model: 向量近似检索模型(如 "lsh");
similarity: 相似度度量方式(如 "cosine");
k: 返回的结果数;
candidates: 候选池大小,用于粗排优化检索效果。
发送请求并解析响应:
请求通过 Elasticsearch _search 接口提交,若返回不成功,则输出报错信息;成功后提取 _source["content"] 字段,返回给上层用于回答生成。
✅ 示例用途:
用户提问:“张三是谁”,系统会将该问题向量化,然后在已有文本向量中进行相似度匹配,从而返回如“张三是法律专家……”的片段,作为构建回答的上下文。
这段逻辑是 RAG 模型“Retriever”阶段的核心,让大模型在“有知识”的基础上作答,提升准确性和实用性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def search(question: str, top_k: int = TOP_K):
qvec = embed(question)
body = {
"size": top_k,
"query": {
"knn_nearest_neighbors": { # 若 Elastiknn 0.7.x 用 elastiknn_nearest_neighbors
"field": "vec",
"vec": { "values": qvec },
"model": "lsh",
"similarity": "cosine",
"k": top_k,
"candidates": 200
}
}
}
r = session.post(f"{ES_URL}/{INDEX}/_search",
json=body, verify=False, auth=ES_AUTH)
if not r.ok:
print("== ES response ==", r.text)
r.raise_for_status()
return [hit["_source"]["content"] for hit in r.json()["hits"]["hits"]]
3.6 主循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# ────────────── CLI 主逻辑 ──────────────
def main():
docs = [
# 原来的 3 条
"张三是法律专家,擅长合同法与知识产权。",
"李四在人机交互领域研究多年,尤其关注可用性测试。",
"王五是一名资深软件工程师,对云原生、DevOps 有丰富经验。",
# 新增 100 条
"赵六是一名数据科学家,专注机器学习模型调优。",
"孙七具有十年金融风控经验,熟悉巴塞尔协议。",
"周八是区块链开发者,擅长智能合约安全审计。",
"吴九长期研究边缘计算,在 IoT 网关架构方面有实践。",
"郑十是资深 DBA,精通 MySQL 性能调优与高可用。",
"钱十一擅长云原生安全治理,主导多家企业零信任落地。",
"蒋十二是 GPU 运维专家,对 CUDA 优化有深入研究。",
"沈十三专注深度学习推理加速,维护 TensorRT 插件。",
"韩十四是 FaaS 平台架构师,关注冷启动优化策略。",
"姚十五从事量化交易算法开发,对高频数据处理熟练。",
"邵十六是渗透测试工程师,擅长 Web 漏洞挖掘与利用。",
"汪十七主攻 AIGC 版权合规,为多家媒体机构提供方案。",
"孔十八研究联邦学习,解决数据孤岛隐私问题。",
"曹十九负责 SRE 团队,精通混沌工程与错误预算管理。",
"严二十是网络取证专家,参与多起重大案件分析。",
"华二一研发 AutoML 平台,降低模型训练门槛。",
"雷二二在 5G 边缘网协同计算领域发表多篇论文。",
"凌二三是 DevRel 经理,推动开源社区增长。",
"史二四对 RAG 架构有深入实践,优化检索召回率。",
"阮二五是 WebGPU 先行者,致力提升前端渲染性能。",
"杭二六主导多云成本治理项目,节省 30% 预算。",
"乔二七是一名 AIGC Prompt 工程师,专精多模态指令设计。",
"詹二八擅长大规模 AB 测试框架落地。",
"顾二九是 Serverless 架构布道者,编写多本技术书籍。",
"龚三十关注 DORA 指标,用数据驱动 DevOps 改进。",
"计三一是 API 网关专家,实现百万 QPS 低延迟。",
"蒲三二研究影像分割模型,用于医学辅助诊断。",
"邱三三是 Zig 语言早期贡献者,推行内存安全编码。",
"庄三四长期维护 Kafka 集群,擅长 Topic 规划。",
"宫三五是低代码平台架构师,关注插件生态。",
"蓝三六研究 ICEBERG 表格式,提升湖仓查询效率。",
"聂三七在安全编排 SOAR 产品设计上经验丰富。",
"陆三八主导 SaaS 产品国际化,本地化流程成熟。",
"温三九负责混合云 DR 方案,实现分钟级切换。",
"袁四十是语音合成工程师,优化多 speaker 适配。",
"贾四一深入研究 DDD,帮助团队理清领域边界。",
"伏四二从事实时风控大数据平台架构,处理亿级流量。",
"程四三是 ARM SoC 驱动工程师,对电源管理熟悉。",
"屈四四在 Federated GraphQL 网关治理方面有案例。",
"申四五带队实现 MLOps 自动化发布流程。",
"罗四六研究 VDBMS,支持 PB 级向量检索。",
"祝四七是 HTAP 数据库布道者,优化混合负载。",
"左四八在 IAM 与 RBAC 设计领域深耕。",
"冷四九是链路可观测性工程师,推广 OTEL 标准。",
"包五十投入异构计算调度框架研究。",
"滑五一精通 eBPF 在安全可观测性场景的落地。",
"柴五二研究量子安全算法,对国密迁移方案熟悉。",
"谈五三是内核安全研究员,发现多个 0-day 漏洞。",
"鄢五四主导 SaaS 计费系统重构,支持灵活套餐。",
"邸五五是绿色数据中心规划师,推动 PUE 降到 1.2。",
"候五六在自动驾驶 SLAM 算法具有专利。",
"古五七关注 CDP 架构,连接多源营销数据。",
"丁五八是 FPGA 加速工程师,实现低延迟推理。",
"靳五九研究 WASM 边缘运行时,降低冷启动。",
"柴六十在 DevSecOps 流水线集成方面经验丰富。",
"花六一策划大规模黑客马拉松,促成 500+ 项目孵化。",
"牛六二是边缘 AI 推理框架作者,重视功耗优化。",
"焦六三研究自监督学习在推荐系统的应用。",
"商六四是 Rust Web 开发者,推广零拷贝 JSON 解析。",
"阎六五投入数字孪生城市平台研发。",
"弓六六主攻 OTA 升级安全,覆盖汽车 ECU。",
"怀六七是 MAC 数据平面专家,优化转发性能。",
"宓六八参与多场灾备演练,完善演练脚本体系。",
"郝六九是 PKI 架构师,设计大规模证书生命周期。",
"嵇七十致力于多媒体编解码标准化。",
"邝七一研究 EDA 自动布线算法。",
"桑七二打造 AI 工厂流水线,实现模型快速迭代。",
"桂七三专注 DPU 加速网络虚拟化。",
"麻七四是 Supabase 中国社区维护者,推广 BaaS。",
"仇七五实现企业级 KYC 流程自动化。",
"薄七六研究多模态情感分析,用于客服质检。",
"谯七七是 SD-WAN 产品经理,聚焦海外专线优化。",
"巫七八负责 Kafka to Pulsar 迁移方案。",
"桑七九在 DAG 引擎优化 CPS 流水线。",
"邬八十研究端侧 LLM 蒸馏压缩。",
"臧八一是三维重建算法工程师,服务文博数字化。",
"禾八二专攻 S3 兼容对象存储网关。",
"原八三参与可信执行环境 TEE 方案落地。",
"淦八四是工业互联网安全规划顾问。",
"练八五实现 GPU 多租户 QoS 调度器。",
"禹八六关注跨境合规要求,精通 GDPR。",
"廉八七是 SDN 控制器开源贡献者。",
"亓八八专注高并发长连接网关。",
"宗八九打造零代码机器学习平台。",
"公冶九十研究 PIM 存内计算架构。",
"红九一是 MESH 网络性能调优专家。",
"眭九二致力于 AI 合成音频版权检测。",
"米九三推动碳排放数据平台建设。",
"隗九四是机器人运动规划算法专家。",
"拉九五研究语义分割在遥感图像的应用。",
"蔺九六负责 0-RTT QUIC 协议优化。",
"臧九七专注 BERT 在法律文本的细粒度实体抽取。",
"昝九八是 RPG 游戏 AI NPC 行为树作者。",
"贝九九研究差分隐私在广告数据的实践。",
"施一百主导云原生 API 安全监控平台。",
"伏一零一优化 Kafka Connect 大规模同步。",
"堵一零二研究车辆 V2X 协议栈实现。",
"莎一零三聚焦 A/B 决策平台可视化。",
]
dim = len(embed("维度探测")) # 动态探测向量维度
create_index(dim)
bulk_upload(docs)
print(f"[√] 初始化完成(向量维度 {dim})。开始提问,Ctrl+C 退出。\n")
try:
while True:
q = input("Q: ").strip()
if not q:
continue
passages = search(q)
context = "\n".join(f"资料{i+1}:{p}" for i, p in enumerate(passages))
prompt = (f"已知资料如下:\n{context}\n\n请根据以上资料回答用户问题:{q}")
print("\nA:", chat(prompt))
print("引用:", passages, "\n")
except KeyboardInterrupt:
print("\nBye!")
if __name__ == "__main__":
main()
4. 运行效果
1
2
3
Q: 张三擅长什么
A: 张三是一名法律专家,专长领域为合同法与知识产权。
引用: ['张三是法律专家,擅长合同法与知识产权。']
结果如下:
完整代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
import os, json, requests, warnings
from ollama import Client
from requests.packages.urllib3.exceptions import InsecureRequestWarning
warnings.filterwarnings("ignore", category=InsecureRequestWarning)
# ────────────── 配置区 ──────────────
ES_URL = os.getenv("ES_URL", "https://localhost:9200")
ES_AUTH = ("admin", "c59a759f31e901e8d279") # 无认证设为 None
INDEX = "rag_demo"
OLLAMA_HOST = os.getenv("OLLAMA_URL", "http://localhost:11434")
EMBED_MODEL = "nomic-embed-text" # 向量模型
CHAT_MODEL = "deepseek-r1:7b" # 对话模型
TOP_K = 4
NUM_CAND = 200 # kNN 先粗召回条数
# ────────────── 初始化 ──────────────
client = Client(host=OLLAMA_HOST)
session = requests.Session()
# ────────────── Ollama 封装 ──────────────
def embed(text: str) -> list[float]:
"""返回文本向量 (list[float])"""
resp = client.embeddings(model=EMBED_MODEL, prompt=text)
return resp["embedding"]
def chat(prompt: str) -> str:
"""与聊天模型对话(完整回复)"""
resp = client.chat(
model=CHAT_MODEL,
messages=[{"role": "user", "content": prompt}],
# stream=False
)
return resp["message"]["content"]
# ---------- ① 创建索引:Elastiknn 映射 ----------
def create_index(dim: int):
mapping = {
"settings": { "index.knn": True },
"mappings": {
"properties": {
"content": { "type": "text" },
"vec": {
"type": "knn_dense_float_vector",
"knn": {
"dims": dim,
"model": "lsh", # 也可 hnsw / exact
"similarity": "cosine",
"L": 99,
"k": 1
}
}
}
}
}
r = session.put(f"{ES_URL}/{INDEX}",
json=mapping, verify=False, auth=ES_AUTH)
if r.status_code not in (200, 201):
if "resource_already_exists_exception" not in r.text:
print("Create index error:\n", r.text)
r.raise_for_status()
# ---------- ② 批量写入:向量必须包 {"values": …} ----------
def bulk_upload(texts: list[str]):
bulk = []
for i, t in enumerate(texts):
bulk.append({ "index": { "_index": INDEX, "_id": i } })
bulk.append({
"content": t,
"vec": { "values": embed(t) } # ★ 关键:对象格式
})
ndjson = "\n".join(json.dumps(d, ensure_ascii=False) for d in bulk) + "\n"
r = session.post(f"{ES_URL}/_bulk",
data=ndjson.encode("utf-8"),
headers={"Content-Type": "application/x-ndjson"},
verify=False, auth=ES_AUTH)
r.raise_for_status()
# ---------- ③ 查询:knn_nearest_neighbors ----------
def search(question: str, top_k: int = TOP_K):
qvec = embed(question)
body = {
"size": top_k,
"query": {
"knn_nearest_neighbors": { # 若 Elastiknn 0.7.x 用 elastiknn_nearest_neighbors
"field": "vec",
"vec": { "values": qvec },
"model": "lsh",
"similarity": "cosine",
"k": top_k,
"candidates": 200
}
}
}
r = session.post(f"{ES_URL}/{INDEX}/_search",
json=body, verify=False, auth=ES_AUTH)
if not r.ok:
print("== ES response ==", r.text)
r.raise_for_status()
return [hit["_source"]["content"] for hit in r.json()["hits"]["hits"]]
# ────────────── CLI 主逻辑 ──────────────
def main():
docs = [
# 原来的 3 条
"张三是法律专家,擅长合同法与知识产权。",
"李四在人机交互领域研究多年,尤其关注可用性测试。",
"王五是一名资深软件工程师,对云原生、DevOps 有丰富经验。",
# 新增 100 条
"赵六是一名数据科学家,专注机器学习模型调优。",
"孙七具有十年金融风控经验,熟悉巴塞尔协议。",
"周八是区块链开发者,擅长智能合约安全审计。",
"吴九长期研究边缘计算,在 IoT 网关架构方面有实践。",
"郑十是资深 DBA,精通 MySQL 性能调优与高可用。",
"钱十一擅长云原生安全治理,主导多家企业零信任落地。",
"蒋十二是 GPU 运维专家,对 CUDA 优化有深入研究。",
"沈十三专注深度学习推理加速,维护 TensorRT 插件。",
"韩十四是 FaaS 平台架构师,关注冷启动优化策略。",
"姚十五从事量化交易算法开发,对高频数据处理熟练。",
"邵十六是渗透测试工程师,擅长 Web 漏洞挖掘与利用。",
"汪十七主攻 AIGC 版权合规,为多家媒体机构提供方案。",
"孔十八研究联邦学习,解决数据孤岛隐私问题。",
"曹十九负责 SRE 团队,精通混沌工程与错误预算管理。",
"严二十是网络取证专家,参与多起重大案件分析。",
"华二一研发 AutoML 平台,降低模型训练门槛。",
"雷二二在 5G 边缘网协同计算领域发表多篇论文。",
"凌二三是 DevRel 经理,推动开源社区增长。",
"史二四对 RAG 架构有深入实践,优化检索召回率。",
"阮二五是 WebGPU 先行者,致力提升前端渲染性能。",
"杭二六主导多云成本治理项目,节省 30% 预算。",
"乔二七是一名 AIGC Prompt 工程师,专精多模态指令设计。",
"詹二八擅长大规模 AB 测试框架落地。",
"顾二九是 Serverless 架构布道者,编写多本技术书籍。",
"龚三十关注 DORA 指标,用数据驱动 DevOps 改进。",
"计三一是 API 网关专家,实现百万 QPS 低延迟。",
"蒲三二研究影像分割模型,用于医学辅助诊断。",
"邱三三是 Zig 语言早期贡献者,推行内存安全编码。",
"庄三四长期维护 Kafka 集群,擅长 Topic 规划。",
"宫三五是低代码平台架构师,关注插件生态。",
"蓝三六研究 ICEBERG 表格式,提升湖仓查询效率。",
"聂三七在安全编排 SOAR 产品设计上经验丰富。",
"陆三八主导 SaaS 产品国际化,本地化流程成熟。",
"温三九负责混合云 DR 方案,实现分钟级切换。",
"袁四十是语音合成工程师,优化多 speaker 适配。",
"贾四一深入研究 DDD,帮助团队理清领域边界。",
"伏四二从事实时风控大数据平台架构,处理亿级流量。",
"程四三是 ARM SoC 驱动工程师,对电源管理熟悉。",
"屈四四在 Federated GraphQL 网关治理方面有案例。",
"申四五带队实现 MLOps 自动化发布流程。",
"罗四六研究 VDBMS,支持 PB 级向量检索。",
"祝四七是 HTAP 数据库布道者,优化混合负载。",
"左四八在 IAM 与 RBAC 设计领域深耕。",
"冷四九是链路可观测性工程师,推广 OTEL 标准。",
"包五十投入异构计算调度框架研究。",
"滑五一精通 eBPF 在安全可观测性场景的落地。",
"柴五二研究量子安全算法,对国密迁移方案熟悉。",
"谈五三是内核安全研究员,发现多个 0-day 漏洞。",
"鄢五四主导 SaaS 计费系统重构,支持灵活套餐。",
"邸五五是绿色数据中心规划师,推动 PUE 降到 1.2。",
"候五六在自动驾驶 SLAM 算法具有专利。",
"古五七关注 CDP 架构,连接多源营销数据。",
"丁五八是 FPGA 加速工程师,实现低延迟推理。",
"靳五九研究 WASM 边缘运行时,降低冷启动。",
"柴六十在 DevSecOps 流水线集成方面经验丰富。",
"花六一策划大规模黑客马拉松,促成 500+ 项目孵化。",
"牛六二是边缘 AI 推理框架作者,重视功耗优化。",
"焦六三研究自监督学习在推荐系统的应用。",
"商六四是 Rust Web 开发者,推广零拷贝 JSON 解析。",
"阎六五投入数字孪生城市平台研发。",
"弓六六主攻 OTA 升级安全,覆盖汽车 ECU。",
"怀六七是 MAC 数据平面专家,优化转发性能。",
"宓六八参与多场灾备演练,完善演练脚本体系。",
"郝六九是 PKI 架构师,设计大规模证书生命周期。",
"嵇七十致力于多媒体编解码标准化。",
"邝七一研究 EDA 自动布线算法。",
"桑七二打造 AI 工厂流水线,实现模型快速迭代。",
"桂七三专注 DPU 加速网络虚拟化。",
"麻七四是 Supabase 中国社区维护者,推广 BaaS。",
"仇七五实现企业级 KYC 流程自动化。",
"薄七六研究多模态情感分析,用于客服质检。",
"谯七七是 SD-WAN 产品经理,聚焦海外专线优化。",
"巫七八负责 Kafka to Pulsar 迁移方案。",
"桑七九在 DAG 引擎优化 CPS 流水线。",
"邬八十研究端侧 LLM 蒸馏压缩。",
"臧八一是三维重建算法工程师,服务文博数字化。",
"禾八二专攻 S3 兼容对象存储网关。",
"原八三参与可信执行环境 TEE 方案落地。",
"淦八四是工业互联网安全规划顾问。",
"练八五实现 GPU 多租户 QoS 调度器。",
"禹八六关注跨境合规要求,精通 GDPR。",
"廉八七是 SDN 控制器开源贡献者。",
"亓八八专注高并发长连接网关。",
"宗八九打造零代码机器学习平台。",
"公冶九十研究 PIM 存内计算架构。",
"红九一是 MESH 网络性能调优专家。",
"眭九二致力于 AI 合成音频版权检测。",
"米九三推动碳排放数据平台建设。",
"隗九四是机器人运动规划算法专家。",
"拉九五研究语义分割在遥感图像的应用。",
"蔺九六负责 0-RTT QUIC 协议优化。",
"臧九七专注 BERT 在法律文本的细粒度实体抽取。",
"昝九八是 RPG 游戏 AI NPC 行为树作者。",
"贝九九研究差分隐私在广告数据的实践。",
"施一百主导云原生 API 安全监控平台。",
"伏一零一优化 Kafka Connect 大规模同步。",
"堵一零二研究车辆 V2X 协议栈实现。",
"莎一零三聚焦 A/B 决策平台可视化。",
]
dim = len(embed("维度探测")) # 动态探测向量维度
create_index(dim)
bulk_upload(docs)
print(f"[√] 初始化完成(向量维度 {dim})。开始提问,Ctrl+C 退出。\n")
try:
while True:
q = input("Q: ").strip()
if not q:
continue
passages = search(q)
context = "\n".join(f"资料{i+1}:{p}" for i, p in enumerate(passages))
prompt = (f"已知资料如下:\n{context}\n\n请根据以上资料回答用户问题:{q}")
print("\nA:", chat(prompt))
print("引用:", passages, "\n")
except KeyboardInterrupt:
print("\nBye!")
if __name__ == "__main__":
main()
小结
EasySearch × Ollama 让我们在本地就能体验到“RAG 的爽点”:检索带来实时、可信的上下文,大模型负责自然语言表达,二者合体即是一个可交付的“企业私有知识助手”。如果你也想在内网快速验证 PoC,这份脚本拷过去改两个地址即可开跑。祝玩得开心!
---
## 我用Amazon Q写了一个Docker客户端,并上架了懒猫微服商店
- URL: https://blog.no-claw.com/posts/8f0212bf/
- 日期: 2025-06-07
- 分类: 懒猫微服 / 番外
- 标签: Docker, 懒猫微服
https://appstore.lazycat.cloud/#/shop/detail/xu.deploy.containly
自从被种草了 Amazon Q,我陆陆续续写了不少小软件,其中这个 Docker 客户端是一个典型的例子,比较符合自己平时使用的习惯,也分享给一些朋友和 NAS 爱好者来用。
故事还要用上次折腾黑群晖说起,本意想把 NAS 和打印机共享二合一的,所以把闲着的软路由做了改装。顺便使用 Docker 跑一些服务,有老本行的 ES 集群,也有自己写的一些工具类型的服务。
随着时间增长,部署的服务多了,时间长了就会忘记服务的端口,甚至还要登录群晖 Web 端进行查看,群晖的 Container Manager 很好用,就是登录的密码策略比较复杂,每次登录都比较麻烦,所以后来使用了一个 HomePage 来保存这些服务。但是每次调试 Docker 都非常麻烦。与 Portainer 相比,我需要的只是一个简洁的面板来查看容器的 URI、状态,并进行启停操作,因此我决定自己开发一个。
这个是群晖的 Container Manager,后面还有很多容器。记住这么多端口然后随时维护绝对不是一个容易的事。
我开发容器面板叫做 Containly, 是一个 Container 的管理工具。最早的时候用我是用 GPT 写的。但是随着项目越来越大,GPT 每次都会丢一些东西,而且还没办法操作本地目录,后来才转向了 Amazon Q,这个版本还是用 Q CLI 来做的。
于是写好之后我把这个 APP 上架了懒猫微服的商店,这个是一款国产化的 NAS,可玩性非常高,对开发者也十分友好。上线当日就有很多开发者安装使用了。
Containly 的核心功能是通过目录映射的 Docker 引擎读取所有容器信息,包括容器的启动、退出、停止及其他状态。例如,当容器处于“Create”状态时,它会被标记为“Other”状态,便于管理。
默认情况下,每个容器卡片会显示容器的网桥信息、端口映射和 URL。默认使用 HTTP 协议,鼠标悬停时,会在右侧显示操作按钮。通过点击这些按钮,操作会被保留,再次点击会隐藏,这样子就整个比较美观。
按钮功能包括:
停止/启动
重启
查看日志
SSH 进入容器
切换 HTTP/HTTPS
黑名单管理
此外,Containly 还提供了一个输入框,用户可以输入需要监控的 NAS 域名,面板会自动根据域名和端口拼接成 URI,并存储在 localStorage 中。更进一步,Containly 还支持暗黑模式,提升了用户体验。
另外如果多节点部署服务的话,还可以把从节点放入黑名单,这样子就只显示主节点的信息,面板就比较清爽。如果需要从节点的信息再从黑名单移除。
利用面板的 SSH 功能, 能够直接从面板进去访问容器的 SHELL,不用执行再 docker exec 的命令。
看日志也很方便,也无需再使用 docker logs,这样调试容器的时候就很方便了。
我已经打包好了 Docker 镜像并配置了 GitHub Actions,便于自动化部署。你可以通过以下方式部署 Containly:
Docker 部署命令
1
2
3
4
5
docker run -d \
--name containly \
-p 5000:5000 \ # 映射容器端口到主机
-v /var/run/docker.sock:/var/run/docker.sock \ # 挂载Docker socket,允许访问宿主机Docker
cloudsmithy/containly:latest # 使用最新版本的Containly镜像
Compose 配置
1
2
3
4
5
6
7
8
9
version: "3.8"
services:
containly:
image: cloudsmithy/containly:latest
ports:
- "5000:5000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
restart: unless-stopped
这个是使用 Q 修改的部分代码截图:
后来机缘巧合之下用了 Q pro,看来也不能优化再多。
除了使用 Q CLI,我们还可以通过安装 VSCode 和 JetBrains 插件来使用 Q,安装插件后,免费版本可以使用 Builder ID 登录,Pro 版本则支持使用 IAM Identity Center 登录。
在 VSCode 中,你可以通过 Q 聊天面板与 Q 进行交互,并且支持中文聊天。
与 GPT 相比,Q 的优势在于它可以直接操作本地文件,用户可以直接在文件夹中生成工程文件,极大提升了开发效率。
---
## 懒猫微服进阶心得(七):懒猫的镜像仓库
- URL: https://blog.no-claw.com/posts/8cdf9a5/
- 日期: 2025-06-06
- 分类: 懒猫微服 / 进阶
- 标签: Docker, 懒猫微服
这几年国内访问 Dockerhub 总有类似的问题,所以很多情况都建议自建 docker 仓库,比如 Habor,Nexus 或者。毕竟 GFW 的花样我们想不到。
云厂商的 ECR。但是对于个人玩家或者爱好者来说这一套实在是太重太难以维护了,可能也就是这个原因,懒猫微服也提供了镜像仓库的和本地仓库的功能。
先说镜像仓库,就是从懒猫微服的服务器上先拉 Docker image,然后再推到自己的 registry。这一步骤通常由出海链路比较好的机器来完成。
懒猫镜像同步功能
懒猫提供了便捷的镜像同步命令:
1
lzc-cli appstore copy-image cloudsmithy/lazycat-nav
执行之后就可以看到镜像仓库,registry.lazycat.cloud 这个地址。
重要说明:
这个地址只能在微服环境中使用,如果在其他地方使用会出现认证错误:
1
2
docker pull registry.lazycat.cloud/u04123229/cloudsmithy/lazycat-nav:854b14e73ab0726e
Error response from daemon: Head "https://registry.lazycat.cloud/v2/u04123229/cloudsmithy/lazycat-nav/manifests/854b14e73ab0726e": no basic auth credentials
其实就一个加了认证的 registry,只是微服有凭证可以直接进。
内置 Docker Registry V2
懒猫微服内置了一个简化版的 registry,完整使用流程如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 构建x86架构镜像
docker build --platform linux/amd64 -t helloworld:latest .
# 获取当前微服名称
BOXNAME=$(lzc-cli box default)
# 为镜像打上仓库标签
docker tag helloworld:latest dev.$BOXNAME.heiyu.space/helloworld:latest
# 推送镜像到仓库
docker push dev.$BOXNAME.heiyu.space/helloworld:latest
# 从仓库拉取镜像
docker pull dev.$BOXNAME.heiyu.space/helloworld:latest
实际操作演示:
在 M2 芯片设备上的构建过程:
在 Orbstack 上拉取验证(先删除本地镜像再拉取):
通过 API 查看镜像列表:
这个简单版本的 docker registry v2,后面用来做跑 CI 的镜像仓库应该是够了。
插曲:
如果遇到这个问题,千万别信 AI 是 buildX 坏了,就是中文路径的问题。(AI 查一小时。Google 一分钟)
1
docker-container:multiarchERROR: failed to dial gRPC: rpc error: code = Internal desc = rpc error: code= Internal desc =header key"x-docker-expose-session-sharedkey" contains value with non-printable ASCII characters
---
## 懒猫微服进阶心得(六):以纸砚双拼为例,快速移植纯前端项目到懒猫微服
- URL: https://blog.no-claw.com/posts/7294709a/
- 日期: 2025-06-05
- 分类: 懒猫微服 / 进阶
- 标签: Docker, 懒猫微服
之前移植了一款颜值极高的纸砚双拼,想着纯前端类的软件应该都是一样的操作,所以把操作记录下来,方便以后复习查看,也相当于一个教程吧。
对于这种场景来说,前端项目开发完成后,我们将构建好的静态页面部署到服务器上。借助 Docker 和 Nginx,可以实现一套轻量、快速、可移植的部署方式,特别适合懒猫微服这种容器化的环境,也很适合日常调试测试使用。
下面将手把手教你如何打包一个 Vue 或 React 项目,并通过 Docker + Nginx 构建一个可复用的前端部署容器镜像,然后就可以把这个部署到懒猫微服删,当然也适用于各种 Linux + Docker 环境。
1 · 准备目录结构
纸砚双拼是 Vue 的项目,所以直接执行这个命令打包
1
2
npm i
npm run build
打包完成后,项目根目录下会生成一个 dist/ 文件夹,里面包含:
index.html
静态 JS、CSS 资源
图片等其他引用资源
基本所有前端工程化的流程都是部署这个 dist/静态目录。
在开始容器化之前,我们先来整理一下项目结构。
1
2
3
4
5
my-static-site/
├── dist/ # 前端打包后的静态资源目录
├── nginx.conf # Nginx 配置文件
├── Dockerfile # Docker 构建文件
└── docker-compose.yml # docker-compose 管理配置(可选)
主要就是把 dist/ 目录映射到 Nginx 的根目录。
.dockerignore(防止把 node_modules 等大文件复制进镜像)
项目根目录建议加个 .dockerignore 文件,防止无用文件进入镜像、浪费空间:
1
2
3
4
5
node_modules
.git
.vscode
*.log
dist
这里的 dist 虽然是构建产物,但因为我们用的是双阶段构建,会在容器里重新生成,不需要提前放入。
2 · Dockerfile(双阶段构建:先构建,再用 Nginx 托管)
我们采用双阶段构建方案,能够最大限度减小最终镜像体积,只包含运行时必须的内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 构建阶段
FROM node:18-alpine AS build
WORKDIR /app
# 复制package.json和package-lock.json
COPY package*.json ./
# 安装依赖 - 使用npm install代替npm ci
RUN npm install
# 复制源代码
COPY . .
# 构建应用
RUN npm run build
# 生产阶段
FROM nginx:alpine
# 复制构建产物到nginx目录
COPY --from=build /app/build /usr/share/nginx/html
# 复制nginx配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 暴露80端口
EXPOSE 80
# 启动nginx
CMD ["nginx", "-g", "daemon off;"]
使用 node:alpine 和 nginx:alpine 轻量镜像,构建出来的镜像体积非常小
分阶段构建,确保生产镜像中没有多余文件
nginx.conf(自定义 Nginx 配置)
Nginx 配置文件如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# 禁止缓存 index.html,确保始终获取最新版本(避免缓存导致更新不生效)
location = /index.html {
root /usr/share/nginx/html;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
# 静态资源缓存设置
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
root /usr/share/nginx/html;
expires 1d;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
3 · 构建与部署
3.1 本地构建测试
执行以下命令构建镜像并启动:
1
2
docker build -t my-frontend .
docker run -p 8080:80 my-frontend
打开浏览器访问 http://localhost:8080,确认页面正常显示。如果用的是懒猫微服,可通过它的 web 浏览器或 ssh 转发方式访问容器。
3.2 可选使用 docker-compose
如果本地调试可以使用
1
2
3
4
5
6
7
8
9
10
version: "3"
services:
cors-tester:
build:
context: .
dockerfile: Dockerfile
ports:
- "80:80"
restart: unless-stopped
之前打包踩的坑
打包多了之后 Docker 会积累不少旧镜像、缓存和挂载卷,下面这些命令能够清理磁盘空间:
使用 pg-docker 或者 lzc-docker 来替代
强制无缓存构建镜像
1
docker compose build --no-cache
不使用任何缓存,适合依赖变动或调试构建问题时使用。
📦 查看磁盘空间占用详情
1
docker system df -v
查看镜像、容器、网络和卷的占用情况,排查“空间去哪了”。
🧹 强制清理所有未使用资源(含挂载卷)
1
docker system prune -af --volumes
删除所有未使用的镜像、容器、网络和卷,释放最大磁盘空间。
注意:慎用,可能会清掉你没保存的 volume 数据。
---
## 懒猫微服实战入门(十三):懒猫穿透不只图形化,纯命令行服务器也可以
- URL: https://blog.no-claw.com/posts/3d7a2d17/
- 日期: 2025-06-04
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服, 组网
需要解析 heiyu.space 这个域名就得安装客户端,突然发现懒猫微服的客户端都是图形化界面。对于服务器环境,特别是没有图形界面的服务器,我们需要纯命令行解决方案。
随着公网 IPv4 地址即将枯竭,许多云厂商的学生机也不再提供公网 IP,这迫使开发者寻找异地组网方案。虽然 Tailscale 是一个可选方案,但作为懒猫微服用户,我更希望利用懒猫自带的组网功能实现这一需求。
在 VIP 群咨询后,获得了服务端组网工具:
https://gitee.com/lazycatcloud/hclient-cli
和花生壳的 CLI 类似,但是比花生壳省心多了。(这里不再过多吐槽花生壳系列了)
安装与配置
1. 下载与初始运行
1
2
chmod +x ./hclient-cli-$arch # 首次启动需要添加可执行权限
./hclient-cli-$arch
初始运行会提示:
当前为非 tun 模式,仅支持通过 http 代理访问微服或其他设备资源
然后就会提示当前为非 tun 模式,仅支持通过 http 代理访问微服或其他设备资源,也就是说现在是单项的组网,这肯定不是我的要求,然后 GPT O3 给了我一个答案。
2. 启用 TUN 模式
1
sudo ./hclient-cli-$arch -tun true
再启动之后,就没有那个 TUN 模式的提示了。如下
3. 常用命令
然后需要使用命令添加,bname 是机器的名字,uid 和 password 是用户名和密码,这样就保证了全球唯一性质,执行完第一步的时候已有的客户端会弹出验证码,执行完第二步就会消失。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 添加微服
curl -X POST 'http://127.0.0.1:7777/add_box?bname=%s&uid=%s&password=%s'
# 设置TFA Code(两步验证码)
curl -X POST 'http://127.0.0.1:7777/add_tfa?bname=%s&tfa=%s'
# 列举微服
curl 'http://127.0.0.1:7777/box_list'
# 删除微服
curl -X DELETE 'http://127.0.0.1:7777/del_box?bname=%s'
# 查看当前客户端信息
curl 'http://127.0.0.1:7777/client_info'
在懒猫微服设备监控中可以看到加入的设备。
2. 访问验证
一开始去访问我写的面食比例计算机,发现了重定向了,才想到懒猫默认给所有的应用加了一个认证。
1
2
3
4
Temporary Redirect.
3. 双向访问测试
商店里的 elasticsearch 放行了所有路由,这样我们在终端和 SDK 就不再需要走那个 web 的验证了,不然只能在请求头里面硬塞 cookie,但是如果是多层认证就非常的难搞。从图片中可以看到,我们从云服务器可以成功访问到家里的懒猫微服了。这个代表从云服务回到家里时没有问题的。
4. SSH 访问测试
其实更多的时候我们的异地组网是为了能够在没有公网 IP 的情况下访问节点,查一下监控设备中的域名,然后 ssh 访问进去,发现基本没什么问题。所以就能够双向访问了,这样我们组网的目的就达到了。
总结
通过懒猫微服的 CLI 工具,我们成功实现了:
无图形界面服务器的穿透接入
双向网络访问
---
## 懒猫微服实战入门(十二):用 iVentoy 打造你的 PXE 服务器
- URL: https://blog.no-claw.com/posts/9d1544e0/
- 日期: 2025-06-03
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服, 电脑外设
很多介绍 Linux 的书籍都会在结尾推荐 PXE 装机方式,尤其是经典的 TFTP + Kickstart 自动部署方案。但在普通家庭或轻量办公环境中,这种方式显得有些繁琐。在 U 盘装机的环境中,我一般使用的 Ventoy多合一 。不过 Ventoy 团队又推出了一个支持 PXE 的装机版本,名为 iVentoy,和 WDS 类似,不过更加省心,这个应用已经上架懒猫微服的应用商店,非常适合内网多机装系统的场景。终于不用再琢磨 WDS 了。还有就是之前不小心用店家带的 U 盘把主机的所有数据都格式化了,这种手残也一去不复返了。
什么是 PXE 无盘装机?
PXE,全称 Preboot eXecution Environment,是一种允许电脑在没有本地操作系统、光盘或 U 盘的情况下,通过网络从服务器下载引导程序并完成系统安装的机制。
PXE 装机简化流程如下:
1
2
3
4
5
6
7
8
9
1. 开机 → BIOS/UEFI 设置为从网卡启动(PXE Boot)
↓
2. 客户端通过网卡发出 DHCP 请求 → 获取 IP 和引导文件地址
↓
3. 通过 TFTP 下载启动文件(如 pxelinux.0 或 iPXE)
↓
4. 加载内核(vmlinuz)和安装器(initrd.img)
↓
5. 进入图形界面或执行自动化系统安装
PXE 装机适用场景
大量办公电脑或服务器的系统部署
设备没有 USB 接口或启动盘时
多系统测试、系统重装、快速恢复环境
各类 PE 工具、Linux Live 系统启动
懒猫微服 上 iVentoy 装机体验
以下是我在懒猫微服环境中使用 iVentoy 的全过程:
1. 上传系统镜像
进入懒猫的应用数据目录:
应用数据 - Iventoy - ISO
将你准备好的 Windows / Linux ISO 文件上传到此路径。
2. 启动 iVentoy,配置网络
确保机器在内网下有一个 有效的 IPv4 地址,然后启动 iVentoy。
3. BIOS 设置启用 PXE
进入待装机设备的 BIOS,确保启用 PXE Boot / 网络启动功能。
4. 插网线,选择 PXE 启动
启动设备,确保插入网线(无线网卡 PXE 启动通常不被支持),从网卡启动。
5. 自动进入 iVentoy 引导菜单
如果网络配置无误,设备会自动弹出引导菜单,可以看到之前上传的 ISO 镜像。
选择一个系统镜像,回车进入即可,相当于将 ISO 当成 LiveCD 使用。
6. 成功进入桌面系统
以 Pop!_OS 为例,系统已经顺利启动,无需任何 U 盘!
小结
这个办法还是适合炫技,把电脑接上网线,然后不用 U 盘,凭空装好系统,不过还是没有 U 盘那么丝滑,有时候不能打满千兆带宽。不过假如手头 U 盘不够用,或者要在多个系统反复装机测试就很方便了。
---
## 懒猫微服进阶心得(五):使用懒猫微服做Ventoy启动盘
- URL: https://blog.no-claw.com/posts/1c49be8c/
- 日期: 2025-06-02
- 分类: 懒猫微服 / 进阶
- 标签: 懒猫微服, 装机
由于经常给别人装系统,所以经常做了一个多合一的系统盘。这今年基本都没用过 windows 了,都是 MacOS + Linux 的组合。但是 Ventoy 本身不支持 MacOS,以前是用 Windows 虚拟机来做这个事情,把 USB 直通进去再烧录。但是最近实在不想碰 Windows,所以想着是不是能在懒猫微服上做这个操作。
下载并解压 Ventoy
1
2
3
4
wget https://github.com/ventoy/Ventoy/releases/download/v1.1.05/ventoy-1.1.05-linux.tar.gz
tar zxvf ventoy-1.1.05-linux.tar.gz
cd ventoy-1.1.05/
./VentoyWeb.sh
这个默认是启动在 localhost,如果需要外网访问改成 0.0.0.0,运行 ./VentoyWeb.sh 后,你会看到如下提示:
1
2
3
4
5
6
7
8
./VentoyWeb.sh
===============================================================
Ventoy Server 1.1.05 is running ...
Please open your browser and visit http://127.0.0.1:24680
===============================================================
################## Press Ctrl + C to exit #####################
翻了翻脚本,是在这里改。
因为我本地通过 dig 解析懒猫微服的域名是 IPV6,所以没办法直接访问,于是我通过 SSH 创建一个本地端口转发(Local Port Forwarding)隧道:
1
ssh -N -L 24680:127.0.0.1:24680 root@lzc
ssh: 启动 SSH 客户端。
-N: 不执行远程命令,仅用于端口转发(即登录后不打开 shell)。
-L 24680:127.0.0.1:24680: 本地端口转发规则,格式为 本地端口:远程地址:远程端口。解释如下:
24680: 本地监听的端口(你访问 localhost:24680 时会触发转发)。
127.0.0.1: 这是 SSH 远程主机上的地址,指代远程主机自己。
24680: 最终目标端口。
root@lzc: 使用 root 用户连接名为 lzc 的主机(可以是域名或 /etc/hosts 里配置的别名)。
当本地访问 localhost:24680,会通过 SSH 加密通道转发到远程主机 lzc 上的 127.0.0.1:24680,就像直接在远程主机上访问一样。
这个就是 Ventoy 的页面了,由于默认懒猫没有 GUI,所以没办法使用类似 windows 那种客户端,还好 Venoty 提供了一个 web 端可以用来玩。
点击右侧的绿色,
接着 Ventoy 会再确认一次是否格式化设备,务必确认盘符无误,这一步会清空整盘数据,后期可以无损升级。
写盘成功的提示,后期只需要把 ISO 直接拖拽进来。
小结
懒猫微服 + Ventoy Web 是制作多合一启动盘的 “无桌面” 优雅方案。
通过 SSH 端口转发,可在不暴露服务的情况下安全地使用 Web 界面。
写盘后只需拖拽 ISO,后续增删镜像都无需重新格式化。
---
## 手把手搞定报名亚马逊科技认证
- URL: https://blog.no-claw.com/posts/4533e9f8/
- 日期: 2025-06-01
- 分类: 软件 / AWS
- 标签: AWS
亚马逊云科技认证考试为我们这些技术从业者提供了提升专业技能的机会。无论选择线上还是线下考试,每种方式都有其独特的优势和挑战。选择合适的考试方式将帮助我们更好地展示自己的技术水平。以下是我对不同考试方式的优缺点介绍,以及各科目的考试代码。希望这些信息能帮助大家在备考过程中做出最优选择。
考试方式介绍
线上考试
优点:
方便快捷:无需通勤,考生可以根据自己的时间安排进行考试。
高灵活性:考试预约灵活,基本上每天都可以进行。
缺点:
环境要求较高:考试时屋内只能有考生一人,且不能有其他声音,否则监考官会关闭考试界面。
网络要求高:需要连接海外的直播平台发送摄像头内容,如果监考官无法看到考生,考试也会被关闭。
线下考试
优点:
环境稳定:无需担心家中环境问题,且由线下监考员监考。
简便的身份验证:只需携带身份证和社保卡即可。
缺点:
考点少且难预约:例如,北京的考点不多且预约困难,多数考点位于郊区,如大兴和昌平。
时间限制:考点通常只在工作日开放,考生可能需要请假参加考试。
各科目考试代码
帮助大家更好地准备和选择考试内容。我总结了亚马逊云科技现在的认证:
报名步骤
首先,您需要访问 https://aws.amazon.com/certification注册一个账户。我建议使用私人邮箱注册,而非公司邮箱,以确保账户的长期可用性。
点击使用 Pearson Vue 进行安排:
在这里可以选择线上、线下和私人访问码,一般选择前两种,第三种通常是与机构合作的链接。
线下考试需要带好身份证,国内还需要带辅助证件,如社保卡或驾照。
线上考试需要保证良好的网络环境和独立的空间,同时准备好身份证。
接下来是选择考场,这里可以同时选择三个考场,然后下一步查看考场的时间:
可以看到最近的日期是 15 号,那么我们可以尝试预约 15 号。
选好时间之后,下一步准备付款。如果有优惠码,可以在下一步输入。这里可以看到考试时间汇总。
付款完成后,可以下载 ics 文件并添加到日历中,这样的话 Apple 日历就会提前提醒我:
优惠和加时
对于非英语母语的考生,可以申请 30 分钟的加时。虽然官网也发布了中文考生可以加时 30 分钟的消息,但尚未说明具体的加时条件。
注意事项
证明身份:线上考试需要身份证或驾照,监考官会帮助新建 case。线下考试需要身份证和社保卡或驾照。
平台:使用 OnVue 平台监考。OnVue 中国区负责报名选考场,如果因网络问题无法考试需要重新预约,技术问题需要联系英文支持。
线上注意事项:检测 WEBRTC 连接和摄像头,考试时全程保持摄像头开启。画面静止或超出摄像头范围,监考官会让退出重进。
Check-in 流程:不能有手表,检查桌面没有其他东西,监考官会询问手机放置位置。
有效期:认证有效期为三年,通过 SAP 考试可自动续 SAA,DOP 可续 SOA 和 DVA,任意助理级考试可续 CLF。
写在最后
线上检录界面,需要做一些准备工作,上传证件,清理桌面之类的工作:
这个是线上和考官的聊天界面:
如果需要申诉,会邮件告知新的抵扣码(原来的考试码作废):
希望这些信息能帮助大家在备考过程中做出最优选择,顺利通过亚马逊云科技认证考试。
---
## 记一次Amazon Q pro的使用
- URL: https://blog.no-claw.com/posts/56bb1eb7/
- 日期: 2025-06-01
- 分类: 软件 / AWS
- 标签: AWS
参加 AWS 的比赛申请了 Amazon Q pro,平时也在用 builderID 登陆使用免费的账户。Amazon Q pro 需要和 IAM identity center 一起用,不过比赛直接给配置好了,直接分发 IAM identity center 的账户,我们只需要注册,登陆,然后关联 Q pro。
从邮箱里给的链接注册,登录。然后绑定 MFA,这个 MFA 其实就是一个二次验证,如果账户被盗,对方没有 MFA 也是无法登录的。
我用了 2Fauth 来绑定的,当然你也可以使用 google authenticator 之类的软件,绑定六位动态码。当然比较常见的 MFA 就是短信验证码,当然还有打电话的。这边刚刚登录,这边电话马上过来。
注册成功会有这个提示。后面需要使用这个绑定的 MFA 进行登录。
登录之后会跳转到这个门户页面,点击 Q 的图标之后会跳转到 Amazon Q 的官方文档。
完成了登录,我们来做本地的配置:
在 VS code 商店中搜索 Amazon Q 并且安装:
Amazon Q 有免费版和 Pro 版。免费版使用 build ID 进行登录,无需 AWS 账户。而今天体验的是 Q pro 版本,还好主办方给配置好了 AWS 的账户,
安装之后右下角就有一个提示登录的弹窗,点击 Sign in,URL 输入邮箱里给的 URL。其实就是 IAM identity center 的登录链接。
然后是跳转浏览器:
以及提示打开浏览器的弹窗:
浏览器打开之后有获取权限的提示:
点击允许之后就大功告成了:
同时 VScode 里的 Q 也会变成这样,最后变成聊天窗口
如图是聊天模式:
如果你使用 Ubuntu server:
下载命令行:
1
wget https://desktop-release.q.us-east-1.amazonaws.com/latest/amazon-q.deb
安装
1
2
sudo apt-get install -f
sudo dpkg -i amazon-q.deb
打开,然后同样的通过浏览器打开:
1
q
如果你用的是苹果系统,就更简单了。从这个链接下载,直接安装就可以了。
https://desktop-release.q.us-east-1.amazonaws.com/latest/Amazon%20Q.dmg
然后在终端中输入 q 或者 Q chat 就可以了。
---
## 懒猫微服实战入门(十一):一键换源指南:用 chsrc 开启丝滑体验
- URL: https://blog.no-claw.com/posts/eef881d4/
- 日期: 2025-06-01
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服
最近懒猫微服的系统固件进行了更新,默认的软件源重新指向了国际源。估计是为了方便海外用户,但对于国内开发者来说,访问速度顿时大打折扣,软件安装和更新频繁卡在连接阶段,着实是一个问题。
正巧群友推荐了一个神器 —— chsrc。这个工具支持一键切换系统、Python、conda 等多个主流组件的源,简洁高效,非常适合懒猫微服这样的轻量环境使用。
下载并安装 chsrc
chsrc 项目在 Gitee 上提供了预编译的可执行文件,适用于不同架构的 Linux 设备。我当前使用的是 x86_64 架构,因此只需运行以下命令:
1
2
curl -L https://gitee.com/RubyMetric/chsrc/releases/download/pre/chsrc-x64-linux -o chsrc
chmod +x ./chsrc
下载后,为了方便使用,我们通常会把它加入环境变量。但懒猫微服的一个特殊机制是:除了 /root 目录,其他目录在每次重启后都会被重置。这意味着如果你将 chsrc 放在 /home 或 /usr/local/bin 之类的目录,它在下次重启后可能就不见了。
所以我将 chsrc 的二进制文件移动到了 /root/app 下(可以新建这个目录),并在 ~/.bashrc 中手动追加了环境变量:
1
export PATH=/root/app:$PATH
这样每次打开 shell 时,系统就会自动把 chsrc 所在路径加入 $PATH,确保我们可以直接使用 chsrc 命令。
一键更换 Debian 软件源
懒猫微服是基于 Debian 的发行版,因此可以直接运行:
1
chsrc set debian
运行后,工具会先检测系统当前源格式(支持新版 deb822),然后测速多个国内镜像源(如清华、中科大、阿里等),自动选择最快的源进行替换,整个过程完全自动化,再也不用去网上搜帖子找各种源了。
更换 Python 镜像源
除了操作系统本身的源之外,也能更换 Python,node 这样的源。
我制作了一个 Miniconda 的一键安装脚本,用来替代系统自带的 Python3.11,用 conda 虚拟环境管理起来很方便。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/env bash
set -e
# 1. 下载最新 Miniconda 安装脚本
echo "📥 正在下载 Miniconda 安装器..."
MINICONDA=Miniconda3-latest-Linux-x86_64.sh
wget https://repo.anaconda.com/miniconda/$MINICONDA -O /tmp/$MINICONDA
# 2. 静默安装到 ~/miniconda3
echo "⚙️ 正在静默安装到 ~/miniconda3..."
bash /tmp/$MINICONDA -b -p $HOME/miniconda3
# 3. 添加到 PATH(写入 .bashrc)
echo "🔧 正在将 Miniconda 添加到 PATH..."
CONDA_INIT='export PATH="$HOME/miniconda3/bin:$PATH"'
if ! grep -q "$CONDA_INIT" ~/.bashrc; then
echo "$CONDA_INIT" >> ~/.bashrc
fi
# 4. 初始化 conda(非交互式)
$HOME/miniconda3/bin/conda init bash
# 5. 提示用户刷新 shell
echo "✅ 安装完成!请运行以下命令以立即生效:"
echo " source ~/.bashrc"
这个脚本的好处是全程自动化,不需要你动手点选目录,也不必一步步设置环境变量,适合懒猫开发者在新设备或系统还原后快速恢复工作环境。
安装好 Miniconda 之后,就可以利用 chsrc 来更换 pip 源了:
1
chsrc set python
此命令会将 pip 的默认源改为清华源或豆瓣源等国内镜像,提高模块下载速度,彻底告别“卡在安装 xx 模块”的窘境。
最后一步,把 conda 的源也换成国内的。直接执行:
1
chsrc set conda
然后需要手动更新 .condarc 配置文件。
小结
这次我体验了 chsrc 在懒猫微服上的完整使用链路,感受可以总结为三句话:
能跑就行?不,要跑得快。
别用默认源,懒人换源有奇效。
系统、Python、Conda,一条龙解决卡顿。
如果你也在用懒猫微服、或者在其他 Debian 系的轻量服务器上摸索开发环境,强烈推荐你试试这个工具和脚本组合。毕竟,“懒得配置”不应该成为“卡在配置”的理由。
---
## 懒猫微服实战入门(十):通过 nmtui 设置静态 IP,接入旁路由实现科学上网
- URL: https://blog.no-claw.com/posts/b704f5f8/
- 日期: 2025-05-31
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服, 代理, 网络
日常开发中,我主要使用的是 MacBook 作为主力机。但有些软件、驱动或容器服务必须在 X86 架构环境下测试,这时懒猫微服就是一个很好的辅助平台。然而在一些网络场景下,为了访问特定服务,还需要将这台设备接入旁路由中。
不然我访问 Dockerhub 是这样的,经常会超时,只能用不是很全的镜像站。
群晖等设备提供的图形化网络面板,懒猫微服这类轻量系统没有 GUI,只能使用命令行工具。NetworkManager 提供了一个非常好用的文本界面工具:nmtui。
什么是 nmtui
nmtui 是 NetworkManager 的 TUI(Text-based User Interface)组件,界面类似简化版 GUI,操作简单,功能却很强大。通过它,我们可以方便地完成以下配置:
选择并连接网络设备(有线或无线)
设置 手动 IP 地址
指定 网关、DNS 服务器
管理 路由策略
开启或关闭 DHCP 自动获取功能
命令如下:
1
nmtui
设置静态 IP 接入旁路由
打开 nmtui 后,进入“Edit a connection”,编辑你连接的网卡(如 enp2s0),手动设置 IP 地址、网关和 DNS。如下图所示,我将所有网络解析都指向了旁路由(比如是 OpenWrt 或其他带翻墙能力的设备):
确保设置完成后,运行以下命令重新应用配置:
1
nmcli device reapply enp2s0
如果配置无误,网络将立即切换到静态 IP 并走旁路由的网关。
验证效果
这样子终端就能下载 Docker image 了,其实到这一步已经完成了。
接下来,可以通过懒猫微服中安装的浏览器测试网络是否生效。
继续打开 YouTube 首页,也能顺利访问并加载视频页面:
你没看错,这是 浏览器里的浏览器,实现了一个“浏览器套娃”的效果。😂
最终,我播放了银临的《牵丝戏》,人美歌也好听~
---
## 懒猫微服实战入门(九):给懒猫安装图形界面,使用 XRDP 实现远程桌面
- URL: https://blog.no-claw.com/posts/8e360f7e/
- 日期: 2025-05-30
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服, RDP
虽然我们日常使用 Linux 系统时,大多数操作都是通过终端完成的,比如运行服务、查看日志、编辑配置文件等。但在某些场景下,图形界面仍然是非常有用的:例如需要打开浏览器进行调试、运行带 GUI 的应用程序,或者想给不太熟悉命令行的同事一个更友好的访问方式。
因此,这一篇教程就来介绍如何在懒猫微服上安装图形桌面环境,并通过 XRDP 实现远程桌面连接。我们选择的是轻量级的 XFCE4 桌面环境,它资源占用小,运行稳定,界面风格有点像老版本 Windows,非常适合资源有限的 VPS 或微型容器环境。当然,你也可以选择 KDE Plasma、GNOME 等更现代的桌面环境,但安装包体积和资源占用会更高。
一键安装脚本
经过多次测试,我总结了一份实用的一键安装脚本,适用于基于 Debian 的系统(如 Ubuntu 或懒猫微服):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/env bash
set -e
echo "==> 设置 keyboard-configuration 为无交互模式"
echo 'keyboard-configuration keyboard-configuration/layoutcode select us' | debconf-set-selections
echo 'keyboard-configuration keyboard-configuration/modelcode select pc105' | debconf-set-selections
export DEBIAN_FRONTEND=noninteractive
echo "==> 更新系统包索引"
apt update
echo "==> 安装桌面和远程桌面组件"
apt install -y xfce4 xfce4-goodies xorg dbus-x11 x11-xserver-utils \
xrdp xorgxrdp keyboard-configuration
echo "==> 配置 .xsession 启动 XFCE"
echo "startxfce4" > ~/.xsession
chmod +x ~/.xsession
echo "==> 添加 xrdp 到 ssl-cert 用户组(避免黑屏)"
adduser xrdp ssl-cert
echo "==> 启动并启用 xrdp 服务"
systemctl enable xrdp
systemctl restart xrdp
echo "✅ 安装完成!现在可以使用 Windows 远程桌面(mstsc)登录本机 IP,使用系统用户名登录 XFCE 桌面。"
你只需要将这段脚本保存为 install-xrdp-xfce.sh 文件,赋予执行权限并运行即可:
1
2
chmod +x install-xrdp-xfce.sh
./install-xrdp-xfce.sh
需要注意的是, 重启之后这个配置仍然会消失.所以还需要使用 systemd –user 拉起来这个脚本.
远程桌面使用说明
安装完成后,在 Windows 系统中打开“远程桌面连接(mstsc)”。
输入你的懒猫主机 IP 地址。
使用系统中的用户名密码进行登录
如果一切正常,应该可以看到一个 XFCE 图形桌面。
如果你连接后出现黑屏问题,通常是 .xsession 配置不正确或者权限不足,上述脚本中已经处理好了这个问题。
补充:如果你还想让它能本地显示图形界面
如果你希望在物理机或有显卡输出的虚拟机中直接打开图形界面(不是远程连接),可以额外添加如下配置:
1
2
3
echo "exec startxfce4" > ~/.xinitrc
chmod +x ~/.xinitrc
startx
这样你在本地终端执行 startx 就能启动 XFCE 桌面。
通过上述脚本,我们可以非常快速地为懒猫微服安装一个可用的图形桌面环境,并通过 XRDP 实现远程访问。这在需要图形界面支持的场景下尤其方便,例如:
使用浏览器调试网页;
可视化工具如 Wireshark、GParted;
为不熟悉命令行的用户开放访问权限。
如果你希望安装 KDE、GNOME 等更复杂的桌面,可以将 apt install 中的包名替换为 kde-standard、gnome 等,并注意资源占用问题。
---
## 懒猫微服进阶心得(四):每次重启都丢配置?用 systemctl --user 自动装回环境!
- URL: https://blog.no-claw.com/posts/95e86a3c/
- 日期: 2025-05-29
- 分类: 懒猫微服 / 进阶
- 标签: Linux, 懒猫微服
在懒猫微服中,为了防止用户误操作破坏系统,默认启用了“重启还原机制”——每次重启都会还原大部分系统改动。不过,用户主目录的数据是保留的(例如 /root/),这就给我们留下了一条生路。
以往每次重启后,我都要手动重新安装 htop、sudo、httpie 等工具,重复操作实在麻烦。之前在 VIP 群里沟通过能否允许使用 systemctl 自启脚本,现在终于支持了 systemctl --user 的开机启动功能,第一时间来体验一下!
💻 安装脚本 init.sh
我们把需要安装的软件统一写进一个脚本,只安装未安装的部分,避免重复浪费时间。同时也支持远程安装一些工具,例如 superfile。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/usr/bin/env bash
set -e # 任意步骤失败立即终止
PACKAGES=(
sudo
htop
wget
build-essential
httpie
exa
duf
bat
# 可以继续添加:docker.io nodejs ...
)
need_install=()
for pkg in "${PACKAGES[@]}"; do
if ! dpkg -s "$pkg" &>/dev/null; then
need_install+=("$pkg")
fi
done
if (( ${#need_install[@]} )); then
echo "==> Installing: ${need_install[*]}"
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y "${need_install[@]}"
else
echo "==> All packages already installed."
fi
# 安装 Superfile 工具
if command -v curl &>/dev/null; then
echo "==> Installing Superfile (from https://superfile.netlify.app)"
bash -c "$(curl -sLo- https://superfile.netlify.app/install.sh)"
else
echo "❌ curl not found, skipping Superfile install"
fi
你可以把这个脚本保存为 /root/init.sh(懒猫微服会保留这个路径),并赋予执行权限:
1
chmod +x /root/init.sh
⚙ systemd 用户服务配置
由于懒猫微服现在支持 systemctl --user,我们就可以通过用户级 systemd 服务在登录后自动执行该脚本。
在 ~/.config/systemd/user/ 目录下创建服务文件:
1
2
mkdir -p ~/.config/systemd/user
vim ~/.config/systemd/user/bootstrap-packages.service
内容如下:
1
2
3
4
5
6
7
8
9
10
11
[Unit]
Description=Bootstrap Required Packages
[Service]
Type=oneshot # 关键修改!表示一次性任务
ExecStart=/root/init.sh
RemainAfterExit=yes # 任务完成后仍标记为 "active"(可选)
# Restart=no # 默认就是 no,可省略
[Install]
WantedBy=default.target
注意事项:
ExecStart 使用 /root/init.sh 是因为懒猫微服重启不会清空 root 目录;
这是一个 oneshot(一次性任务),运行完就退出;
default.target 是用户级别的“登录后启动”目标。
🧪 启用和调试服务
配置好之后,使用以下命令启动并设置自动运行:
1
2
3
4
systemctl --user daemon-reload # 重新加载用户服务配置
systemctl --user start bootstrap-packages.service # 手动运行一次(测试用)
systemctl --user status bootstrap-packages.service # 查看服务状态和日志
systemctl --user enable bootstrap-packages.service # 设置登录后自动运行
示例运行结果:
1
2
systemctl --user start bootstrap-packages.service
systemctl --user status bootstrap-packages.service
输出如下:
1
2
3
4
5
6
7
8
9
10
11
12
○ bootstrap-packages.service - Bootstrap Required Packages
Loaded: loaded (/root/.config/systemd/user/bootstrap-packages.service; enabled; preset: enabled)
Active: inactive (dead) since Thu 2025-05-29 20:13:58 CST; 941ms ago
Duration: 6.438s
Process: 142818 ExecStart=/root/init.sh (code=exited, status=0/SUCCESS)
Main PID: 142818 (code=exited, status=0/SUCCESS)
CPU: 350ms
May 29 20:13:53 lzcbox-029c588e init.sh[142926]: Downloading superfile v1.3.1 for linux (amd64)...
May 29 20:13:58 lzcbox-029c588e init.sh[142926]: Installing superfile...
May 29 20:13:58 lzcbox-029c588e init.sh[142926]: 🎉 Installation complete!
May 29 20:13:58 lzcbox-029c588e init.sh[142926]: You can type "spf" to start!
图示效果如下(安装过程中终端自动拉起):
🚀 小结
步骤
命令
设置 systemd 服务
vim ~/.config/systemd/user/bootstrap-packages.service
测试运行
systemctl --user start bootstrap-packages.service
设置登录自启
systemctl --user enable bootstrap-packages.service
查看运行状态
systemctl --user status bootstrap-packages.service
搭配懒猫微服的 root 持久策略和 systemd 用户服务功能,我们终于实现了:重启自动恢复开发环境,不用每次手动装包了!
---
## 懒猫微服实战入门(八):懒猫微服 × SSH Remote:打造随时随地的云端开发环境
- URL: https://blog.no-claw.com/posts/778337f4/
- 日期: 2025-05-28
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服, SSH, 远程开发
Apple 开始换了 ARM 芯片之后,我们还处于 X86 和 ARM 交叉的阶段。所以有时候还得有一台 X86 的开发环境来测试一些兼容性问题。一开始手里面只有低配置的软路由,后来入手了懒猫微服,这个 11 代 i5 的 CPU 终于能够编译一些重型的任务了。
下面演示如何把懒猫微服配置成一台云端开发机,并分别用 VS Code 与 PyCharm 进行远程开发。
虽然懒猫微服的商店已经上架了 code-sever 可以开箱即用,除此之外我们也来探索下其他的方案。
1 · 准备 SSH 免密登录
修改 ~/.ssh/config(若无自行创建):
1
2
3
4
5
6
7
8
9
Host lzc
HostName server.heiyu.space # 服务器域名 / IP
User root # 默认用户名
IdentityFile ~/.ssh/id_ed25519 # 私钥路径
Port 22 # 默认端口
ServerAliveInterval 60 # 60 秒保活
TCPKeepAlive yes # TCP 保持连接
ForwardAgent yes # 允许代理转发
IdentitiesOnly yes # 只尝试显式钥匙
终端测试:
1
ssh lzc # 应直接登录而不再提示密码
2 · VS Code :一键 Remote
需要 Remote Development 三件套插件(SSH / WSL / Containers)。
安装完成后,点击左下角 棕黄色的 Remote 图标。
VS Code 自动读取 ~/.ssh/config,显示刚才的 lzc 主机。
选择需要打开的文件夹,我们是 root 登录,所以基本上所有的路径都有访问权限,由于懒猫微服的设置,除了 root 目录外,其他的目标可能会被清空,所以我们尽量把代码留在 root 目录下就好。
打开之后我们就可以看到代码了,剩下的就和本地差不多了,这也就是偷懒不学 VIM 的原因吧。
3 · PyCharm :Gateway 秒连
我们再来看 Pycharm,默认提供了远程开发的功能。记得最早 Jetbrains 的是这么实现的,把本地代码推送到远端的/tmp 文件夹然后调用远端编译器执行。现在用了 Gateway 基本可以达到实时的效果了。
依次点击 New SSH Configuration → Add,填入主机、端口、用户名,并选择 私钥 或 密码。
连接成功后,选择需要打开的服务器目录(同样建议用 /root/...)。
首次连接时 PyCharm 会在服务器端安装一个 IDE Agent。
安装完毕即可像本地一样运行、调试、补全。
写在最后
apple 的 M 芯片用来开发,而懒微服就作为上线环境之前的测试环境,当需要 X86 环境的时候,随时切换过去。
---
## 写给懒猫微服玩家的容器小书Docker篇(五):《多容器交响曲:Docker Compose 上场》
- URL: https://blog.no-claw.com/posts/61cfcd24/
- 日期: 2025-05-22
- 分类: 懒猫微服 / 容器
- 标签: Docker, 懒猫微服
一直想写一本容器小书,真好懒猫基本都做了容器化,所以把这部分分享出来。不同的是,懒猫微服中使用 pg-docker 来替代 docker 命令,使用 dockge 来执行 docker-compose。以下讲解以标准 docker 为主,这样子既学会了 docker 知识,也能够在懒猫微服上启动 Docker 服务。
《多容器交响曲:Docker Compose 上场》讲的是使用 Docker Compose 统一编排多容器服务,理解 YAML 配置结构、服务依赖、网络、挂载、构建策略、变量管理、Compose vs K8s 初探等
🎼 开篇:服务之间的管弦乐团
随着项目日益复杂,小李的服务已经不再是一个容器就能承载的了。
前端、后端、数据库、缓存、日志系统……像一个交响乐团,需要统一调度、和谐配合。
老周递给他一个新的工具:“Docker Compose——它是你的指挥棒。”
🎻 第一节:什么是 Docker Compose?
老周解释:
“Docker Compose 是 Docker 的多容器编排工具,用一份 docker-compose.yml 文件,就能同时启动、停止、构建多个服务。”
Compose 帮你解决:
多个服务启动顺序
多容器共享网络
统一管理环境变量
配置简洁、开发者友好
跨平台部署一致
📄 第二节:写出你的第一个 docker-compose.yml
小李的项目结构如下:
1
2
3
4
5
6
7
8
myapp/
├── backend/ # Flask 应用
│ ├── app.py
│ └── Dockerfile
├── frontend/ # 静态页面
│ ├── index.html
│ └── Dockerfile
└── docker-compose.yml
docker-compose.yml 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
version: "3.9"
services:
backend:
build: ./backend
ports:
- "5000:5000"
volumes:
- ./backend:/app
environment:
- DB_HOST=db
depends_on:
- db
frontend:
build: ./frontend
ports:
- "3000:80"
db:
image: mysql:5.7
environment:
- MYSQL_ROOT_PASSWORD=123456
- MYSQL_DATABASE=mydb
volumes:
- dbdata:/var/lib/mysql
volumes:
dbdata:
🔧 每个 service 就是一个容器定义,Compose 会为它们创建默认网络,自动 DNS 互通。
🧪 第三节:Compose 命令实战速查
小李在项目目录下运行:
1
docker-compose up -d
后台启动所有服务!
其他常用命令:
操作
命令
构建镜像
docker-compose build
后台启动
docker-compose up -d
前台启动 + 日志输出
docker-compose up
停止服务
docker-compose down
查看容器日志
docker-compose logs [服务名]
重启某个服务
docker-compose restart 服务名
进入某个容器
docker-compose exec 服务名 bash
📦 第四节:Compose 的网络与数据共享机制
老周介绍:
“Compose 默认创建一个网络,所有服务能通过服务名互相访问。”
在上面的例子中:
backend 容器可以用 db:3306 连接 MySQL
frontend 可通过 backend:5000 访问后端 API
小李不再需要手动 docker network create 和 --network 参数,Compose 一切自动打通。
Volume 的挂载:
Compose 中的 volume 显式声明(如 dbdata:)会自动创建、管理。
支持:
1
2
3
4
volumes:
- ./data:/data # Bind mount
- myvolume:/data # Named volume
- /custom/path:/data:ro # 带权限控制
🌐 第五节:使用 .env 管理配置变量
Compose 支持使用 .env 文件集中管理变量:
.env 文件:
1
2
DB_PASSWORD=123456
DB_NAME=mydb
Compose 文件中使用方式:
1
2
3
environment:
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
- MYSQL_DATABASE=${DB_NAME}
🚀 配合 CI/CD 时 .env 可由流水线动态生成,便于多环境切换(dev/stage/prod)。
🧬 第六节:高级配置技巧
1. 统一重启策略:
1
restart: unless-stopped
确保服务宕机时能自动重启。
2. 多阶段构建支持:
1
2
3
build:
context: ./backend
dockerfile: Dockerfile.prod
可指定构建路径、Dockerfile 文件、构建参数等。
3. Healthcheck 健康检查:
1
2
3
4
5
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 30s
timeout: 10s
retries: 3
🆚 第七节:Docker Compose vs Kubernetes 简析
特性
Compose
Kubernetes
启动容器
简单
标准化
配置语言
YAML
YAML
网络
自动共享
需显式配置
存储
Volume
PVC + SC
服务发现
服务名互通
DNS/ClusterIP
用途
本地开发 / CI
集群部署 / 云原生
高可用 / 伸缩
❌
✅ 内建
社区生态
中小项目广泛使用
大型平台标准方案
小李理解了:Compose 是“轻量乐队指挥”,K8s 是“交响级 AI 指挥系统”。
🔁 第八节:Compose + CI/CD 集成发布
小李将 Compose 整合进 GitLab CI 流程:
.gitlab-ci.yml 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
services:
- docker:dind
stages:
- build
- deploy
build:
stage: build
script:
- docker-compose build
deploy:
stage: deploy
script:
- docker-compose up -d
CI 构建完镜像后,直接用 Compose 部署,既省事又稳定。
🧠 小李 Compose 使用技巧总结
目标
技巧
服务隔离
每个项目单独一个 Compose 文件
共享网络
不用写 network,服务名即 DNS
跨环境配置
使用 .env 动态切换变量
状态排查
docker-compose logs / ps / exec
多环境文件
使用 docker-compose.override.yml
CI/CD 联动
编排命令嵌入流水线任务中
🎬 尾声:指挥容器交响曲的时刻
当小李敲下 docker-compose up 的回车键,前端、后端、数据库同时启动,日志齐鸣,服务稳定运行。
“这就是我梦想的微服务协奏曲啊。”
老周拍了拍他肩膀:
“你已经能用 Compose 驾驭服务之海,下一步,是把它们部署上云、扩展弹性、自动滚动升级。”
小李点头,望向远方的集群编排系统——Kubernetes,眼中闪着新的渴望。
好!那我们就在第五章《多容器交响曲》中,继续深入补充这四大实战技能:
🧩 补充一:Compose 的多环境部署实践(开发、测试、生产)
小李希望同一套服务,在开发、测试、生产环境下分别使用不同的配置,比如:
数据库密码不同
是否挂载本地代码
是否启用调试模式
老周教他使用 Compose 的多文件配置机制:
✅ 方案一:使用 docker-compose.override.yml
Docker Compose 默认会自动加载 docker-compose.override.yml 并与主文件合并。
文件结构:
1
2
3
4
docker-compose.yml
docker-compose.override.yml
.env.dev
.env.prod
主配置(docker-compose.yml):
1
2
3
4
5
6
7
services:
web:
build: .
ports:
- "8000:8000"
environment:
- ENV_MODE=${MODE}
开发环境覆盖文件(docker-compose.override.yml):
1
2
3
4
5
6
services:
web:
volumes:
- ./src:/app/src
environment:
- DEBUG=true
运行:
1
MODE=development docker-compose up
✅ 方案二:按环境拆分多个 Compose 文件
适合 CI/CD 或部署多个 stage。
示例:
docker-compose.dev.yml
docker-compose.prod.yml
运行:
1
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d
多个 -f 会按顺序合并,后面覆盖前面。
📦 建议主文件写“公共配置”,子文件按环境细化。
🎯 补充二:优化服务依赖启动顺序
小李发现,即使写了 depends_on,后端有时候也连不上数据库。
老周摇头说:
“depends_on 只是控制启动顺序,不是等服务就绪。数据库可能还没监听端口就已被标记为 ‘up’。”
✅ 正确姿势:服务内设置“等待就绪”
比如 Flask 等数据库:
1
2
3
4
5
6
7
#!/bin/bash
until nc -z db 3306; do
echo "Waiting for db..."
sleep 1
done
python app.py
或者使用工具包如 wait-for-it.sh:
1
2
COPY wait-for-it.sh /wait-for-it.sh
ENTRYPOINT ["/wait-for-it.sh", "db:3306", "--", "python", "app.py"]
🩺 推荐结合容器健康检查,判断服务是否真正 ready。
🛠 补充三:Docker Compose V1 ➜ V2 迁移技巧
小李的 CI 工具用的是 Compose v1,项目准备升级。
老周提醒:
“Docker Compose v2 使用的是 docker compose(空格),而非 docker-compose(短横线)。”
✅ 主要变化:
项目
v1 (docker-compose)
v2 (docker compose)
命令格式
docker-compose up
docker compose up
安装方式
独立二进制
集成于 Docker CLI
文件格式
v2, v3
推荐统一 v3.9
✅ 迁移建议:
删除旧的 docker-compose 二进制
使用 docker compose CLI
更新脚本、CI 工具调用方式
移除 legacy 字段(如 links)
检查 .env 是否兼容(v2 更严格)
☸️ 补充四:Compose 与 Helm 的映射关系对照
当小李进入 Kubernetes 世界,他问老周:
“Compose 文件和 K8s 的 YAML 有啥对应关系?”
老周说:“很好理解,Compose 是开发者的 K8s 简化版本。”
对照表:
Compose
Kubernetes
services:
Deployment + Pod
volumes:
PersistentVolumeClaim
ports:
Service(NodePort / ClusterIP)
depends_on:
initContainers 或 readinessProbe
.env
ConfigMap / Secret
docker-compose.yml
Helm Chart (values.yaml + templates)
示例:Compose 转 Helm 构思
Compose 配置:
1
2
3
4
5
6
7
services:
web:
image: myapp:latest
ports:
- "8080:80"
environment:
- DEBUG=true
Helm values.yaml:
1
2
3
4
5
6
7
8
9
10
11
image:
repository: myapp
tag: latest
env:
- name: DEBUG
value: "true"
service:
port: 8080
targetPort: 80
Helm deployment.yaml(模板):
1
2
3
4
5
6
7
8
containers:
- name: web
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
env:
{{- range .Values.env }}
- name: {{ .name }}
value: {{ .value }}
{{- end }}
✅ 小李意识到,Helm 是“模板化 + 分层管理”的 Compose 超集,是云原生部署的标准组件管理器。
🎬 尾声:Compose 是微服务上云的跳板
小李已经用 Docker Compose 实现了:
开发环境热更新
测试环境集成数据库
生产环境独立配置
CI/CD 自动部署服务
为 Kubernetes 迁移打下基础
他明白了:
“Compose 就像舞台排练,Kubernetes 才是真正的大型音乐厅。但有了排练,登台才不会慌。”
---
## 写给懒猫微服玩家的容器小书 Docker篇(四):《数据之岛与持久化卷》
- URL: https://blog.no-claw.com/posts/8b5626cb/
- 日期: 2025-05-21
- 分类: 懒猫微服 / 容器
- 标签: Docker, 懒猫微服
一直想写一本容器小书,真好懒猫基本都做了容器化,所以把这部分分享出来。不同的是,懒猫微服中使用 pg-docker 来替代 docker 命令,使用 dockge 来执行 docker-compose。以下讲解以标准 docker 为主,这样子既学会了 docker 知识,也能够在懒猫微服上启动 Docker 服务。
《数据之岛与持久化卷》讲的是 Docker Volume 持久化数据方案、挂载宿主机目录、多个容器共享数据、自动创建卷、数据备份与恢复等
🏝️ 开篇:数据会随浪消失
有一天,小李运行了一个容器,里面的 Flask 项目能正常写入用户信息到 SQLite 数据库。可当容器一停止,再启动——所有数据消失了!
老周说:“你的数据,被潮水带走了。”
“Docker 容器默认的文件系统是临时的,只要容器删除,数据也就没了。想让数据真正存活,就要登上‘数据之岛’。”
🧠 基础概念:数据卷(Volume)
Docker 提供了三种数据持久化方案:
方案
用法
场景
Volume
Docker 管理的专属数据区
最推荐、安全、可多容器共享
Bind Mount
映射宿主机目录
更灵活,适合本地调试
tmpfs
临时存储在内存
适合敏感数据,重启即丢弃
本章重点讲解:Volume(数据卷) 与 Bind Mount(绑定挂载)
📦 第一节:使用 Volume 保存数据
小李重新部署了 MySQL 容器,他决定为数据提供一个“保命空间”。
1
docker volume create mysql-data
然后运行 MySQL 时挂载:
1
2
3
4
docker run -d --name my-mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
-v mysql-data:/var/lib/mysql \
mysql:5.7
-v 卷名:容器内目录:将卷挂载到容器内数据库文件存储位置。
容器即使删除,数据卷依然保留!
查看所有卷:
1
docker volume ls
查看卷详细信息:
1
docker volume inspect mysql-data
输出中可以看到 Mountpoint,即数据在宿主机上的物理位置。
🧪 第二节:自动创建匿名/具名卷
小李写了个简单的服务:
1
VOLUME /app/data
每次 docker run 时,Docker 会自动生成匿名卷挂载到 /app/data。
但这类匿名卷难以追踪、管理,老周建议:
“生产环境请用具名卷,并在运行时用 -v 显式指定。”
🔗 第三节:绑定挂载宿主机目录(本地调试神器)
开发中,小李想把宿主机的项目代码直接挂进容器,不必每次重建镜像。
1
2
3
4
docker run -d --name dev-nginx \
-p 8080:80 \
-v /Users/xiaoli/site:/usr/share/nginx/html \
nginx
本地 /Users/xiaoli/site 的代码实时反映在容器内网站目录,修改立刻生效!
使用 Bind Mount 的场景:
本地开发热更新
日志文件落盘
配置文件映射
IDE + 容器联调
🤝 第四节:多个容器共享数据卷
老周给小李展示了另一个高级玩法:
“多个容器可以挂载同一个卷,共享数据,就像一块公共磁盘。”
小李准备两个容器:
一个容器写入日志
一个容器实时读取日志
1
2
3
4
5
6
7
8
9
# 写入容器
docker run -d --name logger \
-v shared-logs:/logs \
busybox sh -c "while true; do date >> /logs/t.log; sleep 2; done"
# 读取容器
docker run -it --name reader \
-v shared-logs:/logs \
busybox tail -f /logs/t.log
这两个容器在不联网的情况下,通过挂载卷实现了数据同步,让小李直呼神奇。
🔄 第五节:备份与恢复数据卷
老周说:
“你现在的数据安全了,但还不够。万一服务器挂了怎么办?你得学会备份。”
备份数据卷为 .tar:
1
2
3
4
5
docker run --rm \
-v mysql-data:/data \
-v $(pwd):/backup \
busybox \
tar czf /backup/mysql-backup.tar.gz -C /data .
⛴️ 第一个挂载是数据卷,第二个挂载是宿主机当前目录,输出备份包。
恢复数据卷:
1
2
3
4
5
docker run --rm \
-v mysql-data:/data \
-v $(pwd):/backup \
busybox \
tar xzf /backup/mysql-backup.tar.gz -C /data
只需备份 .tar.gz 文件即可,适合迁移数据、升级、容灾。
🧹 第六节:清理无用卷(慎用)
随着实验多了,小李电脑堆满了无主卷。
查看:
1
docker volume ls
清理:
1
docker volume rm 卷名
清除所有未被挂载的孤立卷(慎用):
1
docker volume prune
清理命令要慎重,别误删生产卷!
📋 Volume 挂载选项速查表
类型
命令
特点
匿名卷
-v /path
难追踪,系统自动命名
具名卷
-v myvol:/path
推荐用法,可管理
Bind 挂载
-v /host:/container
与宿主机文件交互,适合本地调试
tmpfs
--tmpfs /path
内存存储,重启即消失
权限控制
-v myvol:/path:ro
只读挂载
SELinux/AppArmor
:z、:Z(高级安全挂载)
安全增强场景
🧠 小李的应用持久化策略建议
类型
内容
数据库
必须挂载 Volume 保持数据持久
日志
推荐落盘到宿主机或集中采集
配置文件
可用 Bind Mount 从本地同步配置
静态资源
静态目录挂载 + CDN
临时缓存
tmpfs 或容器内路径,无需持久化
🎬 尾声:构建自己的数据之岛
小李站在一个小岛码头,身后是一个个挂载卷,他的应用和数据终于脱离容器生命周期的束缚。
老周说:
“真正的服务,要能容器随时销毁,数据却永存。”
小李点头,轻轻拍了拍他那卷 MySQL 的备份包,知道自己已经拥有了构建“数据之岛”的能力。
继续拓展高级内容,围绕:
🧪 数据卷在 CI/CD 中的作用和实战应用
☸️ 数据卷在 Kubernetes 中的延伸 —— PVC(PersistentVolumeClaim)挂载
以小李的旅程为主线,继续带你掌握更强大的容器化数据策略。
🔧 拓展一:数据卷在 CI/CD 中的角色与实践
在一次项目发布中,小李加入了公司的 DevOps 流水线。他发现 GitLab CI 跑测试时,每次都重新构建环境,速度太慢,还会出现缓存丢失的问题。
老周告诉他:
“在 CI/CD 环境中,合理使用数据卷可以极大加快构建速度、保留缓存和数据状态。”
✅ 使用场景 1:缓存 pip/npm 依赖,加速构建
小李把 pip install 改成挂载缓存目录:
GitLab Runner 示例:
.gitlab-ci.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cache:
key: pip-cache
paths:
- .cache/pip
build:
script:
- docker run --rm
-v $CI_PROJECT_DIR:/app
-v $CI_PROJECT_DIR/.cache/pip:/root/.cache/pip
my-builder-image bash -c "
cd /app &&
pip install -r requirements.txt &&
pytest
"
🔁 依赖安装过程可被缓存,构建速度提升 50%!
✅ 使用场景 2:测试数据隔离
小李部署端到端自动化测试容器:
1
2
3
docker run --rm \
-v test-volume:/app/test-results \
e2e-runner:latest
测试结果可从宿主机挂载目录或卷中提取,用于后续报告生成或持久存档。
✅ 使用场景 3:构建产物跨阶段传递
CI/CD 有多阶段:构建 → 测试 → 部署
小李通过挂载共享卷,将编译好的前端包从构建容器传给部署容器:
1
2
docker run --name builder -v build-volume:/output frontend-builder
docker run --rm -v build-volume:/usr/share/nginx/html nginx
☸️ 拓展二:Kubernetes 中的数据卷演进 —— PVC 实战
进入云原生世界,小李不再直接使用 docker run,而是通过 Kubernetes 来编排容器。
在 K8s 中,数据卷概念变得更专业:
Volume(临时存储,随 Pod 生命周期)
PersistentVolume(PV:管理员创建的存储资源)
PersistentVolumeClaim(PVC:用户申请的存储)
StorageClass(存储策略模板)
🔐 场景一:部署 Stateful 服务(如 MySQL)
小李写了以下 YAML:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: standard
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql
spec:
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:5.7
env:
- name: MYSQL_ROOT_PASSWORD
value: "123456"
volumeMounts:
- mountPath: /var/lib/mysql
name: data
volumes:
- name: data
persistentVolumeClaim:
claimName: mysql-pvc
💡 PVC 是 “我要一个 5Gi 的盘”,PV 是“管理员给你一个”,Pod 中挂上它即可持久保存数据。
🧠 小知识点:K8s 中 Volume 的对比
类型
生命周期
是否持久
典型用途
emptyDir
Pod 生命周期
❌
临时缓存、构建产物传递
hostPath
节点目录挂载
⚠️ 有风险
本地开发测试
PVC(推荐)
与集群存储绑定
✅
数据库存储、日志、持久缓存
📦 StorageClass:自动动态分配 PVC
在云环境(如 EKS、GKE)中,PVC 可以自动创建对应的 PV(EBS、Ceph、NFS 等),只要指定 StorageClass 即可:
1
storageClassName: gp2
这种方式可以实现跨节点迁移不丢数据、按需付费扩容、快照备份等高级功能。
✅ 拓展总结:Docker 到 CI/CD,再到 Kubernetes 的数据策略演进
场景
技术
推荐做法
本地开发
Bind Mount
映射目录,实时更新
本地持久化
Volume
隔离性好,便于管理
CI/CD 中间数据
卷 / 缓存目录
挂载 .cache、build 等路径
K8s 中数据存储
PVC + StorageClass
可扩展、可备份、可跨节点持久化
🎬 尾声:从数据孤岛到分布式星图
在 DevOps 流水线中,小李用数据卷构建了无缝衔接的构建流程;
在 Kubernetes 集群中,他用 PVC 实现了跨集群节点的 MySQL 数据持久化部署;
他站在云端存储的星图前,知道自己已经掌握了从 Docker 到 DevOps 再到 Kubernetes 的数据生命周期。
老周拍拍他:
“你已经造好了船,也撑起了帆。下一步,是用 Compose 编排你的舰队。”
---
## 懒猫微服故事篇(一):服务器宕机之后,我和前端靠懒猫微服结对编程
- URL: https://blog.no-claw.com/posts/d2e0eee4/
- 日期: 2025-05-21
- 分类: 懒猫微服 / 故事
- 标签: 开发, 懒猫微服
故事篇分享我和懒猫微服的方方面面,这里没有高深的技术,也没有过多的讲解。我始终坚信着技术是服务于生活,因为它能够给我们带来小确幸。更多的时候,我们追求技术,有时候为了兴趣,有时候为了心目中那小小的执念。慢慢在这个过程中会成为别人眼中的哆啦 A 梦。懒猫微服是一个百宝箱,我们能拿出千变万化的道具。事情会过去,但是感动和记忆会留下,我们可以随时追忆。
和前端同学约好晚上一起过一遍 API,主要是确认 Swagger 上的接口和字段设置。我事先把后端代码、API 和 Swagger 文档都部署在服务器上,让他先通过浏览器简单预览一遍,然后再开始写前端代码,这样能提前避免前后端格式对不上的问题。
准备开始之前,结果他突然说:‘你的 Swagger 打不开了。我登陆到管理控制台看了下,果然和之前部署 Dify 一样的问题,telnet 端口和 Ping 都正常,但是 SSH 和 Web 应用全都访问不了了。从基本监控俩看,CPU 和磁盘也没什么问题。大概率又是 OOM 了。
DDL 赶在眼前,得赶紧让他连到我的服务器或者开发机才好。第一个想到的是端口转发,把服务映射到公网上去,一方面调试的时候属于明文传输,在互联网上很容易被监听,篡改报文。另一方面,家里的公网 IP 经常出问题,总有一阵子会封禁所有的端口,所以最后采取了异地组网的方式。
我和他说,我在我的 NAS 上给你开一个账户,然后你试试来访问我的 Macbook 上的服务。你去下载一个懒猫微服,下载地址是https://lazycat.cloud/download,然后扫描我给你的二维码,这样就可以访问我的NAS了。(回看这段感觉有点像电信诈骗哈哈,感谢前端同学的信任)
他下载 APP、注册账号,我分配权限后,确认他能正常访问微服主页,就开启了端口转发。把我的 Macbook 的地址和 Web 的端口映射出去,只允许已登录的微服客户端访问。这样就免去了被其他的人中间攻击的烦恼,尤记得第一次把服务公开到整个互联网的时候,日志里出现的 IP 真是天南地北,北欧的,中东的,非洲的……
现在用懒猫微服做异地组网,整个流程只需几分钟,再也不用担心这些琐碎的问题了。
他问了我几个问题。
Q1:网址好像打不开。
A1:你打开懒猫微服试试,或者放在后台。需要靠这个软件来做 DNS 解析。
Q2:你的懒猫是什么,软件嘛?还是硬件?
A2:是一个硬件产品,我拍给你,他带了异地组网的功能。
Q3: 还能穿透其他服务嘛?
A3: 除了线下的设备,微服商店里的服务都能穿透,比如使用 planka 来管理进度,有点 Jira 那个味道了。
后面写点感悟,关于云服务器,IDC 和微服
民间几种不同的说法:
前期可以用云做一些 POC,然后稳定了之后就可以搬回自己的机房了。
一切的一切都可以交给云厂商来做,一方面减轻了运维人员的负担,另一方面还可以弹性伸缩,即用即付。
一些传统公司或者是自己有 IDC 的机房会喜欢第一种说法,而云厂商会采取第二种说法。
这并不是否定公有云的价值,但在使用过程中,除了价格高之外,还有一个现实问题:售后支持常常跟不上。国内这些厂商追求短期效应,心思都放在大客户上,用各种加班来满足大客户的需求,当然也包括一些 24K 纯白的需求。而对于处于调研初期的大公司,小微企业,或者说技术爱好者并没有得到平等的对待,甚至的毫不关心的态度,相对于潜在的商单,他们更喜欢数着现在的钱。同样很多传统做 NAS 的厂家售后也跟不上,要么是响应时间等很长很长,或者给到完全不靠谱的结论甚至一本正经的一读乱回。需要注意的是,这里不是一棒子打死所有的云厂商和 NAS 厂家,而是当你遇到紧急的问题时候,还是有很高的概率遇到这样的冷处理。所以我们常开的一句话玩笑话:迁走。
现在觉得,如果不是生产环境必须要对互联网公开或者强制遵循一些安全上的 0 信任原则,比如 POC 环境或者个人环境,用懒猫就完全足够了,还要什么云服务器,那么贵又那么脆。
曾经曾经——我也是云计算忠实的拥护者。。。。。。
---
## 写给懒猫微服玩家的容器小书 Docker篇(三):《容器部落生活》
- URL: https://blog.no-claw.com/posts/5517bd46/
- 日期: 2025-05-20
- 分类: 懒猫微服 / 容器
- 标签: Docker, 懒猫微服
《容器部落生活》讲的是 容器生命周期管理,包括容器的启动、停止、查看、交互、日志、后台运行、重启策略等操作
一直想写一本容器小书,真好懒猫基本都做了容器化,所以把这部分分享出来。不同的是,懒猫微服中使用 pg-docker 来替代 docker 命令,使用 dockge 来执行 docker-compose。以下讲解以标准 docker 为主,这样子既学会了 docker 知识,也能够在懒猫微服上启动 Docker 服务。
镜像旅馆的旅途告一段落,小李的下一站是容器部落。
老周牵着他走过一条闪烁着数字光芒的通道,一排排运行中的应用像帐篷一样排列着。有人在调试日志,有人在重启服务,还有人用 bash 正在某个容器里“打补丁”。
老周说:
“镜像只是静态的模板,容器才是它们的生命。容器是镜像运行出来的真实世界。”
🧠 技术基础:容器 vs 镜像
项目
镜像(Image)
容器(Container)
类比
模板、配方
实际的运行实例
特性
只读
可读写
作用
用来创建容器
实际运行中的程序环境
状态
不运行
可运行、停止、销毁
🛫 第一节:容器的启动方式
小李想运行他的 Flask 应用镜像。
老周告诉他:
“运行镜像的命令是 docker run,容器就像是用镜像盖起来的一顶帐篷。”
1. 最常见方式(临时 + 前台):
1
docker run my-flask-app
但程序一执行完就退出了。小李困惑。
老周解释:
“容器会在主进程结束后自动退出,比如 CMD ["python", "main.py"] 一旦结束,容器就结束了。”
2. 保持后台运行(常用)
1
docker run -d --name flask-app -p 5000:5000 my-flask-app
解释:
-d:detached 模式,后台运行
--name:给容器取个名字,方便管理
-p 宿主端口:容器端口:端口映射,把容器内部 5000 暴露到外部
小李可以在浏览器里访问 http://localhost:5000,服务在跑!
3. 设置环境变量
1
docker run -d -e ENV=production my-flask-app
在容器内可通过 os.environ['ENV'] 访问。
4. 设置自动重启策略(生产强烈建议)
1
docker run -d --restart=always my-flask-app
可选策略:
策略名
说明
no
不自动重启(默认)
on-failure
出错时重启
always
永远重启
unless-stopped
除非人为停止
🧭 第二节:查看容器状态
老周说:“运行中的容器就像是火堆,你得学会看它们是否还在烧。”
查看运行中容器:
1
docker ps
如果想看所有容器(包括已退出的):
1
docker ps -a
输出示例:
1
2
CONTAINER ID IMAGE STATUS NAMES
f123abc456 my-flask-app Up 3 minutes flask-app
🔄 第三节:容器的停止与重启
停止容器:
1
docker stop flask-app
启动容器:
1
docker start flask-app
重启容器:
1
docker restart flask-app
⚠️ 容器停止后并不会删除,除非显式用 docker rm
🧰 第四节:进入容器内“远程调试”
有一次小李发现容器里缺了个配置文件,他想进去看看。
使用 exec 进入运行中的容器:
1
docker exec -it flask-app /bin/bash
-i:保持输入
-t:分配终端
/bin/bash:使用 bash shell(Alpine 镜像可能要用 /bin/sh)
现在他能像 SSH 进服务器一样,在容器里操作文件、日志、环境变量。
📜 第五节:查看容器日志
某天应用崩溃了,小李要调日志。
老周提醒他:“容器日志直接走标准输出和错误输出。”
1
docker logs flask-app
可以加参数看最近内容:
1
docker logs --tail 100 flask-app
实时滚动输出(调试很有用):
1
docker logs -f flask-app
🧽 第六节:删除容器
小李尝试重建容器时,系统提示名字重复。
老周告诉他要先删除原来的:
1
docker rm flask-app
如果容器还在运行,先 stop 再 rm,或直接强制:
1
docker rm -f flask-app
🔄 第七节:容器生命周期一览表
操作
命令
创建 + 运行容器
docker run
后台运行
docker run -d
设置名字
docker run --name name
设置端口映射
-p 外:内
查看容器
docker ps [-a]
停止容器
docker stop name
启动容器
docker start name
重启容器
docker restart name
删除容器
docker rm [-f] name
查看日志
docker logs [-f] name
进入容器
docker exec -it name /bin/bash
🧪 小李的实战练习任务
老周布置了一个练习:
“请你写一个脚本,构建镜像,运行容器,设置环境变量和端口,再用日志确认 Flask 成功启动。”
小李完成如下步骤:
1
2
3
4
docker build -t flask-test .
docker run -d --name flask-run -p 8080:5000 -e MODE=debug flask-test
docker logs -f flask-run
docker exec -it flask-run /bin/bash
🧬 衍生补充:容器的状态码解释
容器退出时,可以用 docker ps -a 查看 STATUS 栏:
Exited (0):正常退出
Exited (1):出错退出
Up:正在运行
调试时很有用。
🎬 尾声:容器部落里的秩序
小李现在已经能够用容器完成:
应用部署
环境隔离
日志查看
重启容错
在线修复问题
他看着容器部落中灯火通明的帐篷,知道自己终于从一个“只会打包镜像”的新人,成长为能驾驭容器生命周期的工程师。
老周拍拍他肩膀说:
“你已经能掌控容器的生死了,下一步——掌控它们的数据。”
好嘞,我们来拓展第三章《容器部落生活》,新增三大实战内容:
🚀 Nginx 容器部署
🔗 多个容器配合运行(应用 + 数据库)
🩺 容器的服务探针(健康检查)
这部分会继续以故事+实战命令+讲解的方式展开,帮助你更贴近真实工作中的容器部署流程。
🧱 实战一:Nginx 容器部署小站点
小李想搭个静态站点测试,老周给他出了一个练习:
“用 Nginx 容器部署一个 HTML 网站,只通过 Docker 完成。”
1. 准备文件结构:
1
2
3
nginx-site/
├── index.html
└── Dockerfile
index.html 示例:
1
2
3
4
5
6
7
8
9
Hello from Nginx
Hello Docker + Nginx!
2. Dockerfile 内容(基于官方 Nginx):
1
2
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/index.html
✅ 把自己的网页文件覆盖掉默认首页。
3. 构建 + 运行:
1
2
docker build -t nginx-site .
docker run -d --name my-nginx -p 8080:80 nginx-site
打开浏览器访问 http://localhost:8080,小李看到页面显示成功,笑得像个孩子。
🔗 实战二:应用容器 + 数据库容器组合运行
接着老周出了第二个任务:
“把你的 Flask 应用和一个 MySQL 数据库用两个容器跑起来,实现数据连接。”
方法一:手动网络 + 多容器连接
1. 创建网络(容器间通信):
1
docker network create flask-net
2. 启动 MySQL 容器:
1
2
3
4
5
docker run -d --name my-mysql \
--network flask-net \
-e MYSQL_ROOT_PASSWORD=root123 \
-e MYSQL_DATABASE=mydb \
mysql:5.7
3. 启动 Flask 应用容器(连接到 MySQL):
假设 Flask 连接数据库时使用:
1
host = 'my-mysql' # 容器名就是主机名
启动命令:
1
2
3
4
5
docker run -d --name flask-app \
--network flask-net \
-e DB_HOST=my-mysql \
-p 5000:5000 \
my-flask-app
🔗 容器间在同一个网络中,通过名字直接通信,像局域网一样!
方法二:使用 docker-compose(下一章会详细讲)
🩺 实战三:为容器添加“健康探针”
老周问小李:“如果你的服务挂了,但容器还在运行,你怎么知道?”
小李摇头:这不就是“僵尸容器”吗?
老周笑了:
“那就给它加一个健康探针(HEALTHCHECK),定期检测服务状态。”
为 Flask 容器加健康检查:
修改 Dockerfile:
1
2
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD curl -f http://localhost:5000/health || exit 1
在 Flask 代码中添加一个健康检查路由:
1
2
3
@app.route('/health')
def health():
return "OK", 200
构建镜像,运行容器后,通过命令查看健康状态:
1
docker inspect flask-app | grep -i health
输出示例:
1
2
3
4
"Health": {
"Status": "healthy",
...
}
如果 /health 接口返回失败状态码,容器会标记为 "unhealthy",可配合自动重启策略联动修复。
🧭 实战拓展总结表
场景
命令 / 技术
部署静态站点
Nginx + COPY index.html
容器间通信
docker network create + --network 参数
多容器组合
应用容器连接数据库容器
服务探针
Dockerfile 添加 HEALTHCHECK,应用实现 /health
探针状态查看
`docker inspect 容器名
🎬 尾声 · 容器集群的微光
小李站在容器部落的山丘上,看着成百上千个容器像城市灯火一样运转。
Nginx 做前端代理,Flask 作为后端逻辑,MySQL 管理数据,每个服务都是一块积木,有秩序、有协作。
老周淡淡说:
“这只是单机的容器调度,真正的战场——在云上。”
小李的眼里闪起光芒:“那我下次要学的就是——Compose,K8s,还有 CI/CD,对吧?”
老周点头:“没错,下一站——数据卷与共享、Compose 编排,再之后……你就要去打云原生的战了。”
---
## 懒猫微服进阶心得(三):一台机器跑三套 Docker?深入解析懒猫容器的共存机制(下)
- URL: https://blog.no-claw.com/posts/b853f136/
- 日期: 2025-05-20
- 分类: 懒猫微服 / 进阶
- 标签: Docker, 懒猫微服
在上一期里,我们剖析了懒猫微服原生的三套 Docker 共存方案,这次我们来看看怎么在懒猫微服上启动新的 dev-docker 引擎,既能拓展使用,但又不与现有环境相互污染。
核心思路
独立 daemon.json 指定专属数据目录 / Socket
一个包装脚本 dev-docker 让你照常敲 docker 命令
需要时随时启用,不用时一条命令即卸载
目录规划
我目前是在 root 目录下新建了一个 dev 目录,新的容器所有数据都在这个目录下。
1
2
3
4
5
6
.
├── dev/
│ ├── data/ # 镜像层、容器元数据
│ ├── exec/ # 运行时文件
│ └── daemon.json
└── dev-docker # 包装脚本,照样敲 `docker`
提示:docker.sock、docker.pid 会在启动时自动生成到 dev/ 里。
生成 daemon.json
这个是主要的文件,定义了 dev-docker 的数据目录,以及命名空间的隔离。
1
2
3
4
5
6
7
8
9
10
11
12
13
# 先拿到绝对路径,避免 dockerd 报相对路径错误
DDIR=$(realpath ./dev)
cat > $DDIR/daemon.json < ./dev-docker <<'EOF'
#!/usr/bin/env bash
export DOCKER_HOST=unix://$(realpath ./dev/docker.sock)
exec docker "$@"
EOF
chmod +x ./dev-docker
设定DOCKER_HOST=unix://$(realpath ./dev/docker.sock),然后用 exec docker "$@" 把收到的全部参数原封不动交给真实的 docker 命令执行。
然后就可以正常使用了:
1
2
./dev-docker ps
./dev-docker run -d -p 8080:80 nginx
系统自带的 docker 仍在 /var/run/docker.sock 上工作,互不打扰。
将 dev-docker 放入全局 PATH
如果想全局生效,运行下方命令。但注意:懒猫微服重启后 /usr/local/bin 会被还原。”
1
sudo install -m 755 ./dev-docker /usr/local/bin/
一键化脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/env bash
# init-docker-dev.sh
set -e
mkdir dev
BASE=$(realpath "./dev")
mkdir -p "$BASE"/{data,exec}
cat > "$BASE/daemon.json" < "./dev-docker" < 即可;
如果之前已将 dev-docker 安装到 PATH,全局也可以直接 dev-docker ps
脚本执行记录如下:
1
2
3
4
5
6
7
8
9
🎉 Dev Docker 已就绪,使用 ./dev-docker 访问!
ubuntu@ip-172-31-29-78:~$ INFO[2025-05-20T12:55:02.071795870Z] Starting up
INFO[2025-05-20T12:55:02.072949048Z] detected 127.0.0.53 nameserver, assuming systemd-resolved, so using resolv.conf: /run/systemd/resolve/resolv.conf
INFO[2025-05-20T12:55:02.157745008Z] Loading containers: start.
INFO[2025-05-20T12:55:02.331021502Z] Default bridge (docker0) is assigned with an IP address 172.17.0.0/16. Daemon option --bip can be used to set a preferred IP address
INFO[2025-05-20T12:55:02.394567874Z] Loading containers: done.
INFO[2025-05-20T12:55:02.413944743Z] Docker daemon commit="26.1.3-0ubuntu1~24.04.1" containerd-snapshotter=false storage-driver=overlay2 version=26.1.3
INFO[2025-05-20T12:55:02.414179613Z] Daemon has completed initialization
INFO[2025-05-20T12:55:02.471933824Z] API listen on /home/ubuntu/ddd/dev/docker.sock
sudo ./dev-docker info查看信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
sudo ./dev-docker info
Client:
Version: 26.1.3
Context: default
Debug Mode: false
Server:
Containers: 0
Running: 0
Paused: 0
Stopped: 0
Images: 0
Server Version: 26.1.3
Storage Driver: overlay2
Backing Filesystem: extfs
Supports d_type: true
Using metacopy: false
Native Overlay Diff: true
userxattr: false
Logging Driver: json-file
Cgroup Driver: systemd
Cgroup Version: 2
Plugins:
Volume: local
Network: bridge host ipvlan macvlan null overlay
Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
Swarm: inactive
Runtimes: io.containerd.runc.v2 runc
Default Runtime: runc
Init Binary: docker-init
containerd version:
runc version:
init version:
Security Options:
apparmor
seccomp
Profile: builtin
cgroupns
Kernel Version: 6.8.0-1024-aws
Operating System: Ubuntu 24.04.2 LTS
OSType: linux
Architecture: aarch64
CPUs: 2
Total Memory: 1.8GiB
Name: ip-172-31-29-78
ID: b6f661de-2099-4b23-aff8-1a55e35833d9
Docker Root Dir: /home/ubuntu/ddd/dev/data
Debug Mode: false
Experimental: false
Insecure Registries:
127.0.0.0/8
Live Restore Enabled: false
./dev-docker pull ubuntu 下载 images:
1
2
3
4
5
6
7
ubuntu@ip-172-31-29-78:~$ sudo ./dev-docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
2f074dc76c5d: Pull complete
Digest: sha256:6015f66923d7afbc53558d7ccffd325d43b4e249f41a6e93eef074c9505d2233
Status: Downloaded newer image for ubuntu:latest
docker.io/library/ubuntu:latest
检查 docker 版本:
1
2
3
4
5
ubuntu@ip-172-31-29-78:~$ dev-docker --version
Docker version 26.1.3, build 26.1.3-0ubuntu1~24.04.1
ubuntu@ip-172-31-29-78:~$ docker --version
Docker version 26.1.3, build 26.1.3-0ubuntu1~24.04.1
不使用的时候如何卸载?
办法 1: ps aux | grep dockerd 查看 docker 进程的 PID 号,然后删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ps aux | grep dockerd
root 470 0.8 0.3 2653088 100248 ? Ssl 07:42 0:11 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
root 2226 6.6 0.6 7246472 227108 ? Ssl 07:42 1:37 /usr/bin/dockerd --config-file /lzcsys/etc/docker/daemon.json
root 27520 0.0 0.2 2874220 90788 ? Ssl 07:46 0:00 /usr/bin/dockerd --config-file /lzcsys/var/playground/daemon.json
root 127241 0.5 0.2 2636632 92720 pts/1 Sl 07:52 0:04 dockerd --config-file=/root/dev/daemon.json --log-level=info
root 405552 0.0 0.0 3748 2048 pts/1 S+ 08:06 0:00 grep --colour=auto dockerd
---
lzcbox-029c588e ~ # kill -15 127241
lzcbox-029c588e ~ # INFO[2025-05-21T08:10:58.184799932+08:00] Processing signal 'terminated'
INFO[2025-05-21T08:10:58.198235413+08:00] stopping event stream following graceful shutdown error="" module=libcontainerd namespace=dev-docker
INFO[2025-05-21T08:10:58.203590577+08:00] Daemon shutdown complete
---
lzcbox-029c588e ~ # ps aux | grep dockerd
root 470 0.7 0.3 2653088 100212 ? Ssl 07:42 0:13 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
root 2226 7.8 0.7 7246472 233080 ? Ssl 07:42 2:18 /usr/bin/dockerd --config-file /lzcsys/etc/docker/daemon.json
root 27520 0.0 0.2 2874220 92644 ? Ssl 07:46 0:00 /usr/bin/dockerd --config-file /lzcsys/var/playground/daemon.json
root 568622 0.0 0.0 3748 2048 pts/1 S+ 08:11 0:00 grep --colour=auto dockerd
办法 2: pkill -f ‘./dev/daemon.json’ 指定文件删除:
1
2
3
4
5
6
7
8
9
10
11
pkill -f './dev/daemon.json'
INFO[2025-05-21T08:14:06.721816466+08:00] Processing signal 'terminated'
lzcbox-029c588e ~ # INFO[2025-05-21T08:14:06.728822927+08:00] stopping event stream following graceful shutdown error="" module=libcontainerd namespace=dev-docker
INFO[2025-05-21T08:14:06.734923834+08:00] Daemon shutdown complete
[1]+ Done dockerd --config-file="./dev/daemon.json" --log-level=info
lzcbox-029c588e ~ # ps aux | grep dockerd
root 470 0.7 0.3 2653088 100340 ? Ssl 07:42 0:14 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
root 2226 8.3 0.7 7246472 234356 ? Ssl 07:42 2:41 /usr/bin/dockerd --config-file /lzcsys/etc/docker/daemon.json
root 27520 0.0 0.2 2874220 92500 ? Ssl 07:46 0:01 /usr/bin/dockerd --config-file /lzcsys/var/playground/daemon.json
root 663902 0.0 0.0 3748 1792 pts/1 S+ 08:14 0:00 grep --colour=auto dockerd
清除数据
1
2
rm -rf ./dev # 删数据目录
sudo rm -f /usr/local/bin/dev-docker # 若装过 PATH
最后
再多一套 Docker,不是为了炫技,而是给开发或者测试环境一个“随时可重置、天然隔离、低成本回收”的保险箱。学会这一招,你就能在懒猫微服乃至任何 Linux 服务器上,放心大胆地尝鲜新内核、新 runtime,甚至复刻生产 bug —— 然后一句 pkill + rm -rf dev/,世界瞬间清爽如初。祝玩得尽兴!
---
## 写给懒猫微服玩家的容器小书 Docker篇(二):《镜像旅馆的秘密》
- URL: https://blog.no-claw.com/posts/e0565a55/
- 日期: 2025-05-19
- 分类: 懒猫微服 / 容器
- 标签: Docker, 懒猫微服
一直想写一本容器小书,真好懒猫基本都做了容器化,所以把这部分分享出来。不同的是,懒猫微服中使用 pg-docker 来替代 docker 命令,使用 dockge 来执行 docker-compose。以下讲解以标准 docker 为主,这样子既学会了 docker 知识,也能够在懒猫微服上启动 Docker 服务。
《镜像旅馆的秘密》讲的是 Docker 镜像的原理、分层结构、生命周期、Docker Hub 上传与下载、常见镜像命令详解
🏰 开篇:进入镜像旅馆
自从小李用 Docker 成功打包并运行了自己的 Flask 项目,他的开发效率飞快提高。
某天,老周带他来到一座巨大的数字建筑——Docker 镜像旅馆。
“这是你所有镜像的家,”老周说,“也是全球程序员共享旅程资源的中转站。”
镜像旅馆里,层层叠叠地存放着成千上万个镜像,就像一栋模块化的高楼大厦。
🧱 镜像的本质:一层一层搭起来的文件系统
老周告诉小李:
“镜像(Image)其实是一个只读的分层文件系统。你写的每一条 Dockerfile 指令,都会构成一层 Layer。”
比如这个简单的 Dockerfile:
1
2
3
4
5
FROM python:3.11-slim
WORKDIR /app
COPY . /app
RUN pip install -r requirements.txt
CMD ["python", "main.py"]
对应的镜像层如下:
FROM → 拉了一个基础镜像层(Python 3.11)
WORKDIR → 添加一个设置工作目录的 Layer
COPY → 拷贝代码文件的 Layer
RUN → 安装依赖的新 Layer
CMD → 容器入口(不是 Layer,但存配置)
💡 小知识:Docker 会尽量缓存和复用前面的 Layer,节省时间和存储。
🧪 镜像命令全攻略
小李打开终端,开始探索这些镜像的日常操作。
1. 查看本地镜像:
1
docker images
输出示例:
1
2
3
REPOSITORY TAG IMAGE ID CREATED SIZE
my-flask-app latest 123abc456def 2 minutes ago 125MB
python 3.11-slim 789xyz654hij 3 days ago 40MB
解释:
REPOSITORY:镜像名
TAG:标签(版本号)
IMAGE ID:镜像唯一标识符
SIZE:镜像大小
2. 查看镜像历史构建过程(看每层):
1
docker history my-flask-app
3. 删除镜像:
1
docker rmi my-flask-app
(⚠️ 若有容器在运行该镜像,需先停止并删除容器)
🗂 镜像仓库:Docker Hub
老周指了指旅馆大堂里的一个巨大电梯:
“这是 Docker Hub,全球最大的镜像共享仓库。”
在这里,小李能下载成千上万的开源镜像,也能上传自己的。
登录 Docker Hub:
1
docker login
(需要先注册账号)
下载镜像:
1
docker pull nginx
这会从 Docker Hub 拉取最新版本的 nginx 镜像
指定版本拉取:
1
docker pull redis:6.2
如果 docker run/pull 有问题,那么可以通过lzc-cli appstore copy-image your-images来使用懒猫的镜像仓库。
(相当于拉取 redis 仓库中 tag 为 6.2 的镜像)
上传镜像(先打标签):
1
2
docker tag my-flask-app yourdockerhubname/my-flask-app:1.0
docker push yourdockerhubname/my-flask-app:1.0
📦 镜像 Tag 与版本控制
老周问:“小李,你知道为什么镜像都有个 :latest 吗?”
小李说:“这是默认版本号吧?”
“对,但我们不能依赖它。开发、测试、生产应使用明确版本号,比如 1.0、20240321 等。”
Docker 镜像是通过 tag 来区分版本的:
1
2
docker build -t myapp:1.0 .
docker build -t myapp:latest .
你可以为同一个镜像打多个标签,对应不同场景使用。
🔍 镜像体积优化技巧
小李注意到镜像越来越大了,占了很多硬盘空间。
老周给了他几点建议:
使用轻量级基础镜像:
比如 python:3.11-slim 代替 python:3.11
合并
1
RUN
命令,减少层数:
1
RUN apt update && apt install -y git && rm -rf /var/lib/apt/lists/*
删除临时文件:
安装后清理缓存,避免垃圾文件残留
多阶段构建(进阶):
构建和运行使用不同的镜像阶段
📂 镜像保存与迁移
后来小李想把自己的镜像传给另一位没有 Docker Hub 的同事。
他用到了镜像导出与导入:
导出镜像为 .tar 文件:
1
docker save my-flask-app > myapp.tar
导入镜像:
1
docker load < myapp.tar
镜像打包成离线文件,便于携带与备份。
🔍 深入 Layer 实战:查看镜像内容
小李很好奇,镜像到底长什么样?
老周教他运行容器并进到里面:
1
docker run -it --rm my-flask-app /bin/bash
这样他就能直接进入容器的 Linux 环境,像在服务器上一样查看文件结构:
1
2
3
ls /
cd /app
cat requirements.txt
他终于明白,每个镜像就像是一个“静态快照”,而容器才是“它的动态运行副本”。
📊 镜像生命周期总结表
操作
命令
查看本地镜像
docker images
构建新镜像
docker build -t name .
删除镜像
docker rmi 镜像名
下载镜像
docker pull 镜像名[:tag]
上传镜像
docker push 镜像名[:tag]
镜像打包导出
docker save > xxx.tar
镜像导入还原
docker load < xxx.tar
镜像历史层查看
docker history 镜像名
🎬 尾声:镜像旅馆的门票
小李现在拥有了多个镜像,搭配不同的版本、依赖、语言,像积木一样可以快速组合各种环境。
“这就像 Minecraft 的世界地图,每张都是一个镜像。”小李说。
老周点点头:“没错,镜像只是开始,真正的冒险——是容器运行起来后的世界。”
🧭 第二章小结
镜像是构建环境的基础模板,支持版本控制、缓存加速、快速构建
可以上传到 Docker Hub 或导出 .tar 进行离线传输
管理命令要熟练掌握:build、pull、push、rmi、tag、history
优化镜像大小要用 slim 基础镜像、合并命令、清理缓存
✨ 增补内容:镜像的高级技能与实战应用
🧪 多阶段构建:精致分工,极限瘦身
有一次,小李需要构建一个使用 npm 打包前端、Python 启动后端的项目。打包工具很多、依赖也重,他担心镜像太大。
老周说:“你要学会多阶段构建(multi-stage build),把构建阶段和运行阶段分开。”
多阶段构建的目标是:编译用谁都行,最终镜像要最小。
示例:Node 构建 + nginx 托管
1
2
3
4
5
6
7
8
9
# 第一阶段:使用 node 构建前端
FROM node:18 AS builder
WORKDIR /app
COPY . .
RUN npm install && npm run build
# 第二阶段:用 nginx 托管打包后的静态文件
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
第一阶段装依赖、打包代码
第二阶段只取编译结果,不用带上 node/npm 等工具
小李一测试,镜像体积从 300MB 降到 25MB,部署速度快了 10 倍!
🧩 使用 .dockerignore:镜像防垃圾机制
构建时,小李发现镜像中夹杂了 .git、node_modules、__pycache__……
老周摇头道:“你忘了 .dockerignore 文件。”
就像 .gitignore 一样,.dockerignore 告诉 Docker 哪些文件在构建镜像时要排除。
示例:
1
2
3
4
5
__pycache__/
.git/
node_modules/
.env
*.log
这个文件放在 Dockerfile 同目录下,能显著加快构建速度和减小镜像大小。
📦 自建私有镜像仓库(Registry)
当公司禁止使用 Docker Hub 时,小李开始尝试搭建自己的镜像库。
老周带他部署了一个本地私有镜像仓库(基于 Docker 官方镜像):
1
docker run -d -p 5000:5000 --restart=always --name registry registry:2
现在他可以:
推送到私库:
1
2
docker tag myapp localhost:5000/myapp
docker push localhost:5000/myapp
拉取镜像:
1
docker pull localhost:5000/myapp
适合公司内部使用,搭配 Nexus、Harbor 可实现更完善的权限、审计、镜像管理等功能。(比如懒猫的 copy-image)
🧠 镜像调试技巧:如何从镜像中探查问题?
如果小李的镜像出错了,他可以通过两种方式“探测”镜像内部:
方法 1:运行一个交互式 shell 容器
1
docker run -it myapp /bin/bash
(如果 bash 不存在,可以用 /bin/sh)
方法 2:打开已有容器的终端
1
docker exec -it container_id /bin/bash
通过 ls、cat、which、env 命令,可以检查:
文件有没有 COPY 进去?
pip install 是否安装成功?
环境变量是否丢失?
🔐 镜像安全:不要把密码打包进镜像!
小李曾在 Dockerfile 里写了:
1
ENV DB_PASSWORD=123456
老周当场拍桌:“你这是把钥匙写死进容器了!”
最佳做法:
在容器运行时注入环境变量(例如使用 .env 文件 + --env 参数)
使用 docker secret 或 KMS 管理
使用 BuildKit 的 --secret 机制加密构建时参数(高级用法)
🧾 镜像标签管理规范建议
小李准备上线,他开始给镜像打各种 tag:
1
2
docker build -t myapp:1.0.0 .
docker tag myapp:1.0.0 myapp:latest
老周说:
“tag 是镜像的版本名,不要用 latest 作为生产环境唯一标识。”
推荐命名规范:
标签
含义
myapp:1.0.0
语义化版本控制
myapp:20240324
构建时间戳
myapp:prod
环境标识
myapp:feature-login
功能分支测试
🔁 镜像缓存失效调试技巧
有时候构建镜像时,小李发现修改了某个文件,Docker 却好像没更新。
老周点拨他:“那是缓存搞的鬼。”
方法一:强制跳过缓存
1
docker build --no-cache -t myapp .
方法二:注意 COPY 顺序影响缓存命中
Docker 会从上到下按顺序缓存。如果把变化频繁的文件 COPY 太早,就会导致缓存失效:
1
2
3
4
COPY requirements.txt . # OK,变动少,适合先复制
RUN pip install -r requirements.txt
COPY . . # 后复制代码,避免频繁无效重建
✨ 技巧:越是稳定的文件,越早 COPY,利于缓存复用。
📘 第二章 · 补充总结更新版
技术点
命令 / 说明
多阶段构建
FROM ... AS builder + COPY --from=builder
忽略文件
.dockerignore 文件
镜像上传私库
docker push localhost:5000/myapp
开启 BuildKit
DOCKER_BUILDKIT=1 docker build ...
进入镜像内调试
docker run -it 镜像 /bin/bash
镜像版本管理建议
避免乱用 latest,使用语义化 tag
跳过缓存构建
docker build --no-cache ...
小李站在镜像旅馆的屋顶,看着一层层高楼像乐高积木一样堆叠而起。
他感到激动——他已经不再为“部署”苦恼,而是拥有了一个随时可打包、可还原的开发宇宙。
老周说:“你的旅程才刚刚开始,容器的世界比镜像更复杂。”
---
## 懒猫微服进阶心得(二):一台机器跑三套 Docker?深入解析懒猫容器的共存机制(上)
- URL: https://blog.no-claw.com/posts/1d9319fb/
- 日期: 2025-05-19
- 分类: 懒猫微服 / 进阶
- 标签: Docker, 懒猫微服
本文仅代表个人视角对懒猫 Docker 的拆解分析,内容为基于现象的倒推推测,不代表懒猫官方实现方式。
拿到任何 NAS 的第一件事是开启 SSH 功能,第二步就是用 Docker 启动容器。
懒猫微服这个 docker 还不太一样,一个有三个 Docker:
docker : 运行系统组件
pg-docker: 普通的 docker,让我们拿来玩
lzc-docker:运行懒猫商店的 docker
三套 Docker 引擎初探
我们先来看看这三套 docker 引擎跑了些什么,从 ps 看起:
docker ps
1
2
3
4
5
6
7
8
9
10
11
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1838d4f379e5 registry.lazycat.cloud/lzc/lzcsys:latest "/sspk/bin/pd-service" 9 days ago Up 9 days lzc-runtime-peripheral-device-1
c2f6d791181b registry.lazycat.cloud/lzc/lzcsys:latest "/sspk/bin/lzc-ingre…" 9 days ago Up 9 days (healthy) lzc-runtime-ingress-control-1
9699c428d2b0 registry.lazycat.cloud/dexidp/dex:v2.42.0-alpine "/usr/local/bin/dock…" 9 days ago Up 9 days lzc-runtime-dex
57952c3e4ba5 registry.lazycat.cloud/lzc/lzcsys:latest "/sspk/bin/lzc-apise…" 9 days ago Up 9 days (healthy) lzc-runtime-api-servers-1
cde0eba62fd2 registry.lazycat.cloud/lzc/lzcsys:latest "/sspk/bin/lzc-pkgm" 9 days ago Up 9 days (healthy) lzc-runtime-pkgm-1
8e9c780c012c registry.corp.lazycat.cloud/homecloud/lzc-registry-proxy:v0.0.0-2887-gd16c7f25.m "/bin/sh -c /lzc-reg…" 9 days ago Up 9 days 80/tcp lzc-registry-proxy
59d3803ef304 registry.corp.lazycat.cloud/homecloud/lzc-installer:v0.0.0-2887-gd16c7f25.m "/docker-entrypoint.…" 9 days ago Up 9 days lzc-installer
c7192a7fd471 registry.corp.lazycat.cloud/homecloud/lzc-hal:v0.0.0-2887-gd16c7f25.m "/bin/sh -c /sspk/bi…" 9 days ago Up 9 days lzc-hal
1d194e975117 registry.corp.lazycat.cloud/homecloud/lzc-recovery:v0.0.0-2887-gd16c7f25.m "/docker-entrypoint.…" 9 days ago Up 9 days lzc-recovery
8338ce6a5c17 registry.corp.lazycat.cloud/homecloud/lzc-recovery:v0.0.0-2887-gd16c7f25.m "/sspk/bin/entrypoin…" 9 days ago Up 9 days
这里可以看到,系统级组件都跑在默认的 docker 下。
pg-docker ps
1
2
3
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d0ae10b8fc8f registry.lazycat.cloud/u04123229/qilinzhu/ql-play:fbf2e99a00ef9a7f "sh /app/start.sh" 3 days ago Up 26 hours ql-play
0cb9ec655c16 registry.lazycat.cloud/u04123229/cloudsmithy/shuangpin:2a8ede2b23c38be8 "/docker-entrypoint.…" 6 days ago Up 6 days 0.0.0.0:5004->80/tcp, [::]:5004->80/tcp unruffled_lichterman
pg-docker 实际上就是日常部署、测试容器最常用的那一套运行时环境, Dockge 默认连接的运行时也是这个。只是这里为了区分系统 docker 做了改名,playground 就是随便玩的意思。
lzc-docker ps
1
2
3
4
lzc-docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
80c88ae6aa8b registry.lazycat.cloud/app-tv-controller:1.0 "/lzcinit/cloud.lazy…" 16 hours ago Up 16 hours (healthy) cloudlazycatapplzctvcontroller-app-1
fdb2211b210e registry.lazycat.cloud/lzc/tvos-release:v0.1.219 "/home/tvos/run.sh" 16 hours ago Up 16 hours 5500/tcp cloudlazycatapplzctvcontroller-tvos-1
这个是懒猫商店的 Docker,实测在客户端中停止应用是是把对应的 docker 删除了,无论是从docker ps -a | grep auth还是可视化工具看来。这也很符合使用容器的习惯,不需要的时候就删除,随用随启动,但是数据仍然还在。
版本和运行时对比
我们再来看一下版本,都还是一样的。所以这个就很有意思了。
1
2
3
4
5
6
lzcbox-029c588e ~ # docker --version
Docker version 27.5.1, build 9f9e405
lzcbox-029c588e ~ # pg-docker --version
Docker version 27.5.1, build 9f9e405
lzcbox-029c588e ~ # lzc-docker --version
Docker version 27.5.1, build 9f9e405
再看看存储后端,那是不是有什么魔改呢?看的出来后端都是 containerd。
1
2
3
4
5
6
7
8
9
lzcbox-029c588e ~ # docker info | grep -i 'Runtimes\|Default Runtime'
Runtimes: io.containerd.runc.v2 runc
Default Runtime: runc
lzcbox-029c588e ~ # pg-docker info | grep -i 'Runtimes\|Default Runtime'
Runtimes: io.containerd.runc.v2 runc
Default Runtime: runc
lzcbox-029c588e ~ # lzc-docker info | grep -i 'Runtimes\|Default Runtime'
Runtimes: io.containerd.runc.v2 runc
Default Runtime: runc
甚至连 containerd 的版本都一样
1
2
3
4
5
6
7
8
9
10
# docker info | grep -i "containerd"
Runtimes: io.containerd.runc.v2 runc
containerd version: bcc810d6b9066471b0b6fa75f557a15a1cbf31bb
lzcbox-029c588e ~ # pg-docker info | grep -i "containerd"
Runtimes: io.containerd.runc.v2 runc
containerd version: bcc810d6b9066471b0b6fa75f557a15a1cbf31bb
lzcbox-029c588e ~ # lzc-docker info | grep -i "containerd"
Runtimes: io.containerd.runc.v2 runc
containerd version: bcc810d6b9066471b0b6fa75f557a15a1cbf31bb
lzcbox-029c588e ~ #
一开始以为是魔改看了下我的 mac 运行的 Orbstack 的配置,好像也没啥差别。
1
2
3
4
5
6
7
8
9
10
11
docker info | grep -i 'Runtimes\|Default Runtime'
Runtimes: io.containerd.runc.v2 runc
Default Runtime: runc
docker info | grep -i "containerd"
Runtimes: io.containerd.runc.v2 runc
containerd version: 06b99ca80cdbfbc6cc8bd567021738c9af2b36ce
多引擎共存的实现方式
DOCKER_HOST 的封装
既然三个 docker 都出奇的一致,到底是类似命名空间的隔离嘛?
1
2
3
4
5
6
7
8
9
10
11
12
lzcbox-029c588e ~ # which docker
/usr/bin/docker
lzcbox-029c588e ~ # which pg-docker
/lzcsys/bin/pg-docker
lzcbox-029c588e ~ # which lzc-docker
/lzcsys/bin/lzc-docker
lzcbox-029c588e ~ # file $(which docker)
/usr/bin/docker: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=4a41bb12cfd0c306a6ede40f41cfc107b2045371, for GNU/Linux 3.2.0, with debug_info, not stripped
lzcbox-029c588e ~ # file $(which pg-docker)
/lzcsys/bin/pg-docker: Bourne-Again shell script, ASCII text executable
lzcbox-029c588e ~ # file $(which lzc-docker)
/lzcsys/bin/lzc-docker: Bourne-Again shell script, ASCII text executable
这就可以发现问题了,docker 是原来的 docker,但是 pg-docker 和 lzc-docker 是封装的脚本,来看一下:
所以我们可以得出一个关键点:懒猫并不是运行了三套完全独立的 Docker 服务,而是通过 shell 脚本封装,复用同一个 docker 客户端,切换不同的 socket 实现了“环境隔离”。这个脚本的作用相当于把 pg-docker 当成 docker 命令使用,还自动附带了环境变量 DOCKER_HOST=...。
1
2
3
4
5
6
lzcbox-029c588e ~ # cat $(which pg-docker)
#!/bin/bash
set -e
export DOCKER_HOST=unix:///data/playground/docker.sock
exec docker "$@"
这设置了 DOCKER_HOST 环境变量,使得之后执行的 docker 命令会连接到 /data/playground/docker.sock 这个 Unix Socket,而**不是默认的 /var/run/docker.sock**。
exec 是一个 shell 内建命令,它会用新的进程替换当前脚本的进程。
"$@" 表示把脚本接收到的所有参数(比如 pg-docker ps -a)原样传递给 docker 命令。
之前想上架一个 Docker 可视化工具用来,但是总不知道需要映射哪个 docker.sock,这下子全都清楚了,有了这个就能在 docker 里使用宿主机的 Docker API 了。
daemon.json 配置详解
当然与之对应的还有 daemon.json,除了用来改代理之外,我们还能修改这些东西:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
lzcbox-029c588e /data/playground/data # cat /lzcsys/var/playground/daemon.json
{
"bridge": "",
"containerd-namespace": "playground-docker",
"containerd-plugins-namespace": "playground-docker",
"data-root": "/data/playground/data/docker",
"default-address-pools": [
],
"exec-root": "/data/playground/docker",
"hosts": [
"unix:///data/playground/docker.sock"
],
"insecure-registries": [
"registry.lazycat.cloud"
],
"pidfile": "/data/playground/docker.pid"
这样多个 Docker 环境就能共存了,例如:
系统默认的 /var/run/docker.sock
一个沙箱环境 /data/playground/docker.sock
这么设置好之后可以快速切换上下文,而不用每次都手动设置 DOCKER_HOST。
在我开发的容器可视化面板总,看到已经可以指定 docker sock 作为连接了,参考:
1
2
3
4
5
6
7
8
services:
containly:
image: registry.lazycat.cloud/u04123229/cloudsmithy/containly:30e4e3279afe9a52
ports:
- 5003:5000
volumes:
- /data/playground/docker.sock:/var/run/docker.sock
restart: unless-stopped
三套 daemon.json 对比分析
从实际查找结果来看,懒猫为三套 Docker 引擎配置了不同的 daemon.json 文件和运行时环境:
系统组件专用(docker):/etc/docker/daemon.json
用户 playground 环境(pg-docker):/lzcsys/var/playground/daemon.json
懒猫商店环境(lzc-docker):/lzcsys/etc/docker/daemon.json
1
2
3
4
5
6
7
8
9
sudo find / -type f -name daemon.json 2>/dev/null
/etc/docker/daemon.json
/run/lzcsys/boot/lzc-os-init/var/playground/daemon.json
/run/lzcsys/boot/lzc-os-overlay/lowerdir/lzcsys/etc/docker/daemon.json
/run/lzcsys/boot/lzc-os-overlay/lowerdir/lzcsys/lzcsys/etc/docker/daemon.json
/lzcsys/etc/docker/daemon.json
/lzcsys/var/playground/daemon.json
每个配置文件中都指定了独立的:
data-root
exec-root
pidfile
hosts(即 sock 文件路径)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 默认的docker引擎
lzcbox-029c588e ~ # cat /etc/docker/daemon.json
{
"registry-mirrors": [
],
"insecure-registries": [
"registry.lazycat.cloud"
],
"log-driver": "journald",
"cgroup-parent": "sys_docker.slice"
}
# 商店的docker引擎
lzcbox-029c588e ~ # cat /lzcsys/etc/docker/daemon.json
{
"bridge": "none",
"insecure-registries": [
"registry.lazycat.cloud"
],
"default-address-pools": [
],
"ipv6": true,
"hosts": [
"unix:///lzcsys/run/lzc-docker/docker.sock"
],
"containerd-namespace": "lzc-docker",
"containerd-plugins-namespace": "lzc-docker-plugins",
"exec-root": "/lzcsys/run/lzc-docker/docker",
"pidfile": "/lzcsys/run/lzc-docker/docker.pid",
"data-root": "/lzcsys/run/data/system/docker",
"cgroup-parent": "lzc_docker.slice"
}
# playground的docker引擎
cat /lzcsys/var/playground/daemon.json
{
"bridge": "",
"containerd-namespace": "playground-docker",
"containerd-plugins-namespace": "playground-docker",
"data-root": "/data/playground/data/docker",
"default-address-pools": [],
"exec-root": "/data/playground/docker",
"hosts": [
"unix:///data/playground/docker.sock"
],
"insecure-registries": [
"registry.lazycat.cloud"
],
"pidfile": "/data/playground/docker.pid"
小结
懒猫微服总会给我惊讶,除了极客风格的外壳,性能突出的硬件外,里面的软件设计也同样优秀,这个设计让我对 docker 有了更加深刻的认识。
三套 docker 共存不是表面上的魔改,而是通过 containerd 的 namespace 配合脚本封装,在容器之上再抽象一层运行时,把 playground、系统和商店隔离成三界,却又共用一套内核,很好玩。
---
## 如何关闭 AWS root 账号的 MFA 认证
- URL: https://blog.no-claw.com/posts/f4ce40dc/
- 日期: 2025-05-19
- 分类: 软件 / AWS
- 标签: AWS, MFA
警告:关闭 root 账号的 MFA 认证存在较高风险。AWS 通常不建议此操作,因为 root 账号拥有对所有 AWS 资源的完全控制权限。如果密码或账号信息泄露,可能导致严重的安全事故。
关闭步骤
使用 root 账号登录 AWS 管理控制台
点击右上角账号名称
选择”安全凭证”选项
在页面中找到”多重身份认证(MFA)”部分
点击”删除”即可
然后会收到邮件提醒:
如果遇到这个报错,那么重新登陆之后再重复前面的步骤:
1
To complete this action, please ensure that you are authenticated with an MFA device that is enabled for this user.
---
## 懒猫微服实战入门(七):超越蒲公英SD-WAN,用懒猫转发服务,Ipad随时远程家里window
- URL: https://blog.no-claw.com/posts/59f4c56d/
- 日期: 2025-05-19
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服, 组网
最近懒猫用懒猫的穿透服务突然有一些想法,既然每个客户端都可以用域名连接到微服,那么有没有可能从客户端之间可以互相访问呢?于是抱着这个目的试了一下,确实可以。异地组网的原因主要有几个,没有公网 IP,传输链路容易被截获以及不同办公室需要逻辑上的组网。
说干就干。
在懒猫论坛上看玄兴梦影的文章,《懒猫微服无缝连接你所有的设备》和《懒猫微服助我生活工作穿梭无忧》这两篇让我受益颇丰。主要是借用懒猫微服的中继服务,用来自建 Rust Desk,还有 RDP 到局域网的其他设备上。
先说 Rust Desk,这是一度被推举为 Todesk 的替代的软件,在懒猫上安装服务端拿到域名和密钥,然后在客户端上填入就可以了,具体操作可以看前面推荐的文章,里面有细致的讲解。
我的需求就是,在外边除了能够访问家里的微服之外,还访问其他的设备,比如群晖,威联通,甚至 windows。这样子以后带个 Ipad 出门就可以了,MacbookPro 的受害者表示笔记本太沉了。
所以这本质上是一个异地组网的问题,在上一个版本的 HomeLab 中是使用的蒲公英的 P5 盒子,这东西卖点是旁路由,直接 POE 接入局域网就好了,还能共享打印机。但是吧,商业产品还有很多限制,比如组网只能三个设备,带宽有限制,在多次的和售后拉扯而且案例没有在规定时间内回复也没有按照服务水平协议赔偿之后,索性退坑。
还有一次很玄学的事情,蒲公英盒子升级固件和家里跳闸的时间高度重合,开案例询问之后,传了日志,客服也只是贴了文档表示绝对不会有电流突增的问题,再问就再也没有回复了。然后会自动结单给五星好评,还不能重新打开继续问问题。
蒲公英在国内是没有什么竞品的,论企业服务远远不及思科之类的产品,而个人用户又一种丝毫不在意的态度。而且会一直推荐买最新的 X5 PRO,而卖完之后改完静态路由不能组网,客服又一副到底你懂不懂的样子。哎,蒲公英是彻底疯转黑了。那不买他们硬件用 OrayOS 呢?请先看免责声明:
所以一直想找其他 SD-WAN 或者异地组网的方案。主要需求嘛,就是既能从外面访问,也能转发其他的设备的流量,能做权限控制就更好了。
懒猫微服目前没有自己的虚拟机系统,所以是用的 webvirtcloud,但是商店里也有很多上架的系统,比如直接点点鼠标就可以开 windows 虚拟机了,这个有点云计算的味道了。不过就是我用的机械硬盘,开 windows 实在是有点差强人意。所以后面弄了一个局域网的机器专门拿来跑 windows,就是后面要提到的局域网转发。
从商店下载的 app 都有一个特定的域名。
1
<应用名>.<设备名>.heiyu.space
以商店里的虚拟机为例,直接在 RDP 里输入这个机器的 URL 就就行,以前的 Windows remote desktop 现在改名叫做了 Win APP,同时也能在国内的 app store 上搜索到了。(前提登录懒猫 APP)
首先设置密码,虽然没有密码能够打开远程登录,但是实际怎么都连不上。
然后在系统设置打开远程桌面,在上面那个搜索栏的中直接搜就好。 然后我们就可以连接了。
那两台登录微服的设备如何互联呢?这里有个很赞的工具,可以获取客户端的信息。商店搜索:懒猫微服在线设备获取
在 APP 中我们能够看到每个设备的域名:(隐私保护,我这里已经隐去)
那在脑洞一下,是不是可以做到之前说的旁路由的效果呢?
这里有个局域网转发工具。
猜测可能是 iptables 一类转发的吧。可以把局域网的设备映射出去(不需要安装懒猫客户端),还要什么旁路由。
现在远程连接的是我这台局域网一台刚刚装好的 windows,不需要在被控端安装安装任何软件(包括懒猫 APP),看到也能通过懒猫的域名组网了,开心~
内网穿透有了!
旁路路模式转发有了!
出门带 ipad 不用带笔记本了~
---
## 写给懒猫微服玩家的容器小书 Docker篇(一):《无法部署的诅咒》
- URL: https://blog.no-claw.com/posts/effe7a9a/
- 日期: 2025-05-18
- 分类: 懒猫微服 / 容器
- 标签: Docker, 懒猫微服
一直想写一本容器小书,真好懒猫基本都做了容器化,所以把这部分分享出来。不同的是,懒猫微服中使用 pg-docker 来替代 docker 命令,使用 dockge 来执行 docker-compose。以下讲解以标准 docker 为主,这样子既学会了 docker 知识,也能够在懒猫微服上启动 Docker 服务。
《无法部署的诅咒》讲的是 Docker 的起源与作用、镜像与容器概念、安装入门
🏙️ 开篇:代码在电脑上运行良好,部署却频频翻车
小李是一名后端工程师,刚完成一个用 Flask 编写的内部管理系统。他信心满满地将代码提交,交给测试工程师老赵部署。
可没想到——
“报错了!你是不是少传文件了?”
“我这边 Python 是 3.6,怎么你这代码用了 match case?”
“你数据库呢?你 Redis 配了?环境变量在哪?”
这一刻,小李陷入了一个叫「部署地狱」的世界。
这个世界里,每一台服务器都是独一无二的“宠物”,需要手动配置、手动部署、手动踩坑,稍有不慎就会陷入版本冲突、依赖缺失、环境不一致的深渊。
小李心里想:有没有一种办法,能让我把代码和环境一起打包起来,无论在哪运行都能保持一致?
🌀 变故:神秘的程序员大叔与魔法容器
在公司茶水间,小李邂逅了一个带着黑框眼镜的大叔,大家都叫他老周。
老周是个技术老炮,在各种部署大灾难中杀出血路。他听完小李的吐槽后,轻轻地推了一份文档过来,只写了五个字母:
Docker
“这是个魔法容器,”老周说,“把你代码和环境都打包进去,哪怕放在火星,也能跑。”
“以后别再‘它在我电脑上可以跑’了,Docker 能让所有环境变得一致。”
小李的眼睛亮了。
🔧 技术讲解 Part 1:什么是 Docker?
Docker 是一个开源的容器化平台,它允许开发者将应用及其所有依赖打包成一个“容器”,保证在任何平台上都可以一致运行。
你可以把 Docker 想象成:
程序员的打包箱子:把你写的程序、环境、库、配置都放进去,打包成一个“镜像”;
程序员的快递服务:运行镜像就像打开快递,内容和你寄出时一模一样。
🚀 为什么要用 Docker?
✅ 跨平台运行:一次构建,到处运行(Run anywhere)
✅ 快速部署:秒级启动,适合 CI/CD
✅ 环境一致性:不再“你电脑能跑我电脑不行”
✅ 轻量隔离:不像虚拟机那么重,不需要整个操作系统
✅ 资源占用少:用起来更像一个进程,而不是一台虚拟机
☑️ 一句话总结:Docker 解决了“在我电脑上能跑”的问题。
🛠️ 技术讲解 Part 2:如何安装 Docker?
老周拍了拍小李的肩膀,说:“先装起来,动手最重要。”
🧑💻 在 Mac / Windows 上:
访问官网:https://www.docker.com/products/docker-desktop
下载并安装 Docker Desktop
安装后打开终端(Terminal),输入:
1
docker --version
如果看到 Docker 的版本信息,说明安装成功。
🧑💻 在 Linux(Ubuntu)上:
1
2
3
4
sudo apt update
sudo apt install docker.io
sudo systemctl start docker
sudo systemctl enable docker
🔐 建议将当前用户加入 docker 组,以免每次都要用 sudo:
1
sudo usermod -aG docker $USER
🧠 技术讲解 Part 3:Docker 的核心概念
概念
说明
镜像(Image)
应用和其依赖的静态快照模板(如:打包好的 Python 环境 + Flask 项目)
容器(Container)
镜像运行出来的实例,是真正“活着”的程序
Dockerfile
编写镜像的“配方”,定义如何构建镜像
Docker Hub
类似 GitHub 的公共镜像仓库,可上传/下载别人做好的镜像
docker 命令
Docker 的主命令工具,用于操作镜像、容器、网络、卷等
📌 镜像 vs 容器
镜像是“模具”,容器是“实物”
镜像不可变,容器是可运行的环境
一个镜像可以运行多个容器
🧪 技术实践:Hello Docker 世界
老周指导小李敲下第一行命令:
1
docker run hello-world
如果 docker run/pull 有问题,那么可以通过lzc-cli appstore copy-image your-images来使用懒猫的镜像仓库。
这个命令会做三件事:
自动从 Docker Hub 拉取一个 hello-world 镜像(如果本地没有)
基于镜像运行容器
容器运行后输出信息,然后自动退出
这是验证 Docker 是否正常运行的“点灯测试”。
🧱 小李的第一个真实容器:Flask Web 项目打包实战
老周笑着说:“现在,把你那个 Flask 管理系统也丢进 Docker 试试。”
小李在项目根目录下写了一个 Dockerfile:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 使用 Python 官方基础镜像
FROM python:3.11-slim
# 设置工作目录
WORKDIR /app
# 复制项目文件
COPY . /app
# 安装依赖
RUN pip install -r requirements.txt
# 设置启动命令
CMD ["python", "main.py"]
构建镜像:
1
docker build -t my-flask-app .
启动容器:
1
docker run -p 5000:5000 my-flask-app
现在访问浏览器 http://localhost:5000,你的项目上线了!
💡 小知识快闪
Docker 容器内部不包含完整操作系统,只包含必要的运行库;
Dockerfile 的每一行都是一层(Layer),构建时会缓存复用;
docker ps 查看运行中容器,docker images 查看已有镜像;
可以通过 .dockerignore 文件忽略不想加入镜像的文件,比如 .git 和日志。
⚔️ 章节尾声:打破部署诅咒
这次,小李把打好的镜像发给了测试老赵:
1
docker run -p 5000:5000 my-flask-app
老赵只运行了一行命令,项目便神奇地跑了起来。
“不改配置?不用装 Python?数据库也连上了?”
“你这是什么魔法!”
小李笑了笑,第一次感觉部署是件简单的事。
🧭 第一章总结知识点:
技术点
命令
安装 Docker
Mac/Win 下载 Docker Desktop / Ubuntu 安装 docker.io
查看版本
docker --version
运行测试容器
docker run hello-world
编写 Dockerfile
FROM / COPY / RUN / CMD
构建镜像
docker build -t name .
运行容器
docker run -p 宿主:容器 镜像名
---
## 懒猫微服炫技篇(一):使用懒猫微服的穿透服务,车机使用DS music 远程听歌,黑群晖不用再洗白
- URL: https://blog.no-claw.com/posts/12695041/
- 日期: 2025-05-18
- 分类: 懒猫微服 / 炫技
- 标签: 懒猫微服, NAS
声明:炫技篇不是最佳实践,只是为了记录过程和那颗折腾的心。虽然很多时候可以用钱来升级或者多忍一忍就好了。但是折腾的心始终是不安分的,技术在职场中证明不了什么,但是在生活中可以。当晦涩的理论不仅仅存在于书本,才能给我们带来更大的价值。这个过程中能够找到我们到底是为了什么才需要这项技术,问题的痛点,以及这技术的前世今生。。。。。。
前些天给老旧的车载导航升级了 WIFI 功能,大致就是出厂的时候锁了 wifi 的驱动和 UI,然后通过 ADB 解锁的,然后通过路由器 Mesh 升级了老旧的 APP 以及导航数据,这次又带来了全新的功能,给普通车机升级 Carplay。
毕竟我的初衷,就是想在车机上安装懒猫 APP 而已啊。
Part 1:
下载懒猫微服 APK,通过 U 盘导入安装,报错:解析错误,解析软件包时出现问题。
于是和懒猫相关技术进行确认,很快 CEO 给到了答复,目前只支持比亚迪和华为。毕竟国产车嘛,路子野一点也正常。
那我如果不用安卓,换 Carplay 呢?是不是就能用 IOS 的客户端直接投过去了?
Part 2:
开始改 Carplay。
车载导航是当时赠送的,安卓 6.0。朋友推荐说可以购买 Carplay 盒子,于是疯狂搜索,现在的 Carplay 盒子分为两种。
有线 Carplay 改无线 Carplay
安卓车机改无线 Carplay
我的安卓车机没有 Carplay APP,方向盘也没有语音控制按钮。所以一开始能不能支持 Carplay,我是比较有疑问的。直到搜到了一篇夸赞车连易的帖子,大概说是最早做这个方向的公司,产品比较稳定一类的。
于是买回来调试,需要先安装他们的 autokit 软件检测(买之前需要用 Iphone 检测),这个 APP 后续当 Carplay APP 使用。(这里有个小插曲,之前 adb 调试的时候在改了工厂模式,开了 USB 调试,导致后来 USB 无法识别任何设备,包括 U 盘,2.4G 发射器,手机,以及 Carplay 盒子)对照之前的照片,关了就好了。然后我就拥有了人生第一台 Carplay 车机,盒子一点不烫,就是手机哗哗掉电。
开心之余还不忘记去找懒猫 APP,屏幕上有钉钉,百度网盘,网易云,甚至连 Home Assistant 都有?那我的懒猫微服 APP 呢?原来是 Apple 的白名单限制,美其名曰为了安全。GPT 搜下了,基本上除了导航和音乐一类的设备其他都没有了。那我高速服务区追个剧?好歹让投个屏嘛。。。。。。
同时和微服以及 Carplay 盒子的厂家确认,得到了一份寥寥无几的 APP 名单。
梦碎?
GPT 的解释如下:
以下内容基于 Apple 开发文档(2024 年最新版)及最近一次 iOS 18.4 更新的公开信息整理而成,可作为「CarPlay 现阶段 官方 支持的 App 类型、代表性应用,以及常见限制」速查表。(Apple Developer)
1 | CarPlay 允许上车的 8 大官方类别
类别
典型代表 App
主要交互模板
备注
导航
Apple 地图、Google Maps、Waze、Gaode/高德
地图、仪表盘、列表
iOS 18.4 起可设 默认导航(EU 地区率先落地)(The US Sun)
音频
Apple Music、Spotify、网易云音乐、Podcast、Audible、DS Music(Synology)
播放中、网格、列表
仅音频;禁止歌词滚动、视频封面等会分散注意力
通信
电话、信息、WhatsApp、钉钉、Teams
列表、语音
文本内容不得直接显示,只能通过 Siri 朗读
停车
EasyPark、停简单
列表
提供空位、计费等信息
EV 充电
Tesla、ChargePoint、小桔充电
列表、地图
可筛选功率、空闲桩等
加油 / 加氢
BPme、壳牌加油
列表、地图
仅提供站点 & 价格;支付流程需遵循「停车后才允许」规则
快餐外卖
Dunkin’、星巴克
列表
只能选择常用餐点并一键复购,禁做复杂自定义
驾驶辅助 / 任务
扫盲测试、行车记录提示类
网格、信息
聚焦安全提醒、里程统计等(不含 ADAS 改装)
车企自家 App(空调、座椅、360 环视等)属于「Automaker Apps」,与上表并列但须由车厂提交。(Apple Developer)
2 | 为什么某些 App(视频、文件管理等)上不了 CarPlay?
场景
限制要点
开发侧原因
视频/娱乐(Netflix、哔哩哔哩)
行车安全:禁止播放分散注意力的可视内容
CarPlay 模板不提供视频区域;审核会拒绝
文件管理(ES 文件浏览器、NAS 探索)
无对应类别;操作路径过复杂
无可用模板且易诱导司机操作
社交媒体(微博、X、抖音)
不得显示滚动 Feed、评论等
CarPlay 指南明令「No social networking」(Apple Developer)
游戏 / 浏览器
驾驶分心 & 没有类别
同上
3 | 作为开发者,要满足哪些门槛?
申请 CarPlay Entitlement
向 Apple 提交 App ID、类别说明、演示视频;获批后才能编译带 CarPlay 功能的版本。(Apple Developer)
使用系统提供的模板
只能调用列表、网格、地图、Now Playing 等固定 UI;不可自绘按钮或随意布局。(Apple Developer)
全部操作无需拿起 iPhone
登录、付费等「复杂流程」必须设计为停车后才可完成,或通过 SiriKit 语音处理。
严格的内容审核
不得插入横幅广告,禁止收集车辆数据做非核心用途,消息不可明文显示等。
4 | 对普通用户而言的使用限制
维度
具体表现
设备要求
iPhone 6s 及以上(iOS 17+ 建议),Lightning 有线或支持无线 CarPlay。
同时运行数量
iOS 18.4 支持「三排图标」;实际显示取决于车机分辨率。(The US Sun)
地区差异
某些导航/支付/餐饮 App 仅在特定国家可上车,例如中国区暂不开放「Apple 钱包加油」。
多任务
非导航类 App 在后台仅获有限音频/定位权限;切回主屏超过 8 分钟或手动关闭即结束会话。
常见 Q&A
问题
解答
能在 CarPlay 打开 NAS 上的影片吗?
不行,现阶段只支持「音频类」DS Music;视频需停车使用原车 USB/HDMI 或车企自带系统。
想在车机上用 VSCode、ES 文件浏览器?
属于生产力 / 文件管理场景,CarPlay 无对应类别,无法过审。
越狱或使用 CarBridge 能装任何 App 吗?
理论可行但高风险:系统不稳定、Apple Pay 安全受损、保修被拒,不建议在主力机尝试。
官方 仅开放 8 大类别,核心目标是「行车安全 + 切实刚需」。
想把新 App 带到 CarPlay?先判断是否符合这 8 类,然后按模板开发并申请 entitlement。
作为用户,若某款 App 还未上车,说服力最大的途径是:去 App Store 给开发者留言催更,而不是等「万能破解」。
Part3
小红书上搜到了 DS audio 的攻略,竟然是群晖套件?评论区还有人推荐 DS music,颜值很高。下载之后我也很喜欢。
不就是群晖嘛,这年头谁还没一个黑群晖了。公网 IP 不定时被封端口,那懒猫转发了解一下?
于是把群晖的端口映射出来,就在 APP 中可以远程连接使用了,这不比 Synology QuickConnect 还省事嘛。。。。
各类软件测试如下
DS audio 一直提示登录,尽管手机端已经一切正常
DS music 可以正常访问和听歌,这个软件颜值爱了
DS music+ 一直加载失败,
DS cloud 和 DS player 在打开时候好像是一样的 UI,能找到歌曲,但是点击没反应
最后喜欢的还是 DS music,功能正常还好看啊。
除了 DS 系列,好像 Q 系列也行,不过吃灰很久很久了。(具体连接待测试)
用懒猫微服解决了公网转发的问题,再也不担心被运行商封端口了,还有车载大屏(除了有点掉帧),也算圆了一个梦吧。
你的车有 Carplay 吗?
---
## 懒猫微服实战入门(六):使用2fauth共享你的MFA
- URL: https://blog.no-claw.com/posts/b6b74923/
- 日期: 2025-05-16
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服, MFA
标题有点绕口,甚至听起来有点反直觉。
故事的背景是这样的,去参加了 AWS 的活动给的账户强制开 MFA,但是我们还想团队内部 share 使用,于是产生了这个需求。
登录到 AWS 的控制台强制开了 MFA,而且在第一次注册的时候强制绑定多因子验证。这也就意味着,其他人如果想登录这个账户就得随时 call 我,然后我去发给他数据验证码,这实在很不方便,所以想到了共享的 MFA 的需求。
头几天逛商店看到的,觉得项目有点意思就下载了,没想到这么快用到了。懒猫商店,一键部署很方便,当成 Saas 服务来用,完全不考虑部署运维的事情。
之前给小伙伴开了懒猫微服的账户,共享了 planka 来看项目进度,这次把 2fauth 的权限也添加给他。
首先我这边先注册管理员的账户,默认是登录页面,需要切换一下。
登录之后会提示绑定一下这个账户的 MFA,我就是为了不在手机上安装 MFA 软件才用这个的,就不要套娃了。反正外面还有懒猫的验证系统,那个还有 TLS 加密,安全码验证。
选择不绑定设备之后,在这里导入需要设置的 MFA,这可以用摄像头或者导入二维码文件。我用的电脑端,所以直接在应用处截图,然后导入到这里了。
点击最下面的导入,然后选择二维码 - 上传 就可以了。
导入之后是这样的,可以二次确认签发机构。
然后把生成的 6 位数字填写到 aws 控制台上,就可以成功验证了。
在 2fauth 控制台上是这样的,点开就可以查看 6 位数字验证码。
那么回到一开始的话题,怎么共享给其他账户呢?点击下方 - 管理员 - 用户 ,然后我们来新建一个普通用户。步骤基本和前面的一致。
本来以为有用户组一类的概念,把两个用户和 MFA 放在一个组里达到 share 的目的,结果发现这个分组完全是用来区分的 TAG。也没有找到把用户加到组里的操作。那就从管理员导出,再从下一个用户导入吧。
首先试了二维码,但是导入的时候就提示 server error。于是查了了 wiki,都是其他 MFA 软件导入 2fauth 的。无奈只能只能导出配置文件。名字叫做 2fauth_export.json
登录新用户的时候新建,然后选择文本文件。导入刚才的配置文件就可以了。
配置文件基本长这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"app": "2fauth_v5.3.2",
"schema": 1,
"datetime": "2025-05-16T08:35:07.676665Z",
"data": [
{
"otp_type": "totp",
"account": "Q",
"service": "AWS SSO",
"icon": null,
"icon_mime": null,
"icon_file": null,
"secret": "secretsss",
"digits": 6,
"algorithm": "sha1",
"period": 30,
"counter": null,
"legacy_uri": "otpauth://totp/"
}
]
}
整个过程有点绕,有人说每个人手机安装 google authenticator 扫一下不就好了吗?
为什么采取这个方案?
之前用手机安装类似软件,每次去三里屯维修的时候都说返厂要把数据抹掉,下次还得重新绑定,还有一些软件只认 MFA 不认人。
这过程不光折腾的够呛,而且 Apple 本身的问题还要 MFA 来买单。
起初是想做一个类似于团队共享 MFA 的场景的,类似于 RBAC,控制起来很灵活,但是实际体验下来是没有达到的。
把最早的 MFA 二维码截图 share 出去也能扫,但是不确定有效时间。
---
## 深度长文:NAS大降价的年代,我为何接受溢价来购买懒猫微服(附送回本攻略)
- URL: https://blog.no-claw.com/posts/d6c5e7b2/
- 日期: 2025-05-15
- 分类: 懒猫微服 / 番外
- 标签: 懒猫微服, NAS
最早知道懒猫微服,是去年的时候,那个时候最直观的感觉就是价格比同类型产品要贵一些,但是很极客风,不过硬件配置比传统 NAS 要高出很多。但在现在各种小主机盛行的年代,这台机器就显得性价比不高,甚至有些人认为有割韭菜的嫌疑。
今年朋友又过来推荐,说是售后很好,可以根据自己的需求来答疑,比如把三方监控放在存在 NAS 里,比如想用私服搭建游戏服务器等等,而网上不管怎么样,还有说情绪价值一定给拉满的。
第一次咨询的时候,是和 CEO 通了个电话,抱着将新将疑的态度购买回来,拆箱,测评。相信其实很多人即使没听过王勇,也一定听过或者用过 Deepin。大学的时候使用过一段时间的 deepin,很多细节确实符合国人的使用习惯。这个背书对于技术人来说,实在是一下子路转粉。想想自己在电话里还跟对方说,其实专业的技术人员,不用和我说这么直白的词,再想想王总在 Deepin 以及 Emacs 方面的贡献, 实在是有些惭愧。
我本身是开发者,问题是技术细节相关的,比如为何这个实现和群晖类不一样,对某处设计比较反常识的地方询问和拆解,总的来说,像是上了侠客岛一样,平时自认为是开发人员里面最懂 Infra 的,结果到这里谁的 Linux 都比我玩的好。
目前重度使用了一周多,每天都会在 VIP 群里问问题。主要把维护的问题解决出来,好像附送了一个终身的云厂商支持一样。从他们的宣传来看,7 _ 18 的支持显得更加实在一些,服务相比海底捞有过之无不及。我本身用过不少 7 _ 24 小时支持的云厂商,要么低峰时候找不到人,而 24 小时支持又何尝不是对技术从业者的压榨呢?比如随时 on-call,倒班机制是我本人深恶痛绝的,有些厂家号称是 7 * 24 小时支持,但是经常已读乱回要么不回,或者干脆说这个问题和他们产品的交叉是涉及第三方,然后索性不管了。有意思的是,当使用两个公司交叉的业务时,都要让我去找对方。但是在懒猫这里就不会发生这样的问题,之前用的商店里的 dify 有问题,他们去找移植应用的人去修改了。
还有一个卖点,是硬件终身售后,甚至包括磁盘和后续的数据恢复(前提是不加密),有些推吞吐量高要求的情况甚至可以做 Raid0,然后外接 NAS 或者硬盘仓备份,所以这不是一款后端存储的产品,而是放在存储和用户之间的加速器,作为家庭的边缘算力,前面接 MBP 后面接存储池这样子。
商店目前上架了 1000+的应用,虽然官方的应用不多,但是很多三方应用都是他们的开发人员移植的,于是后来才有了越来越多的开发者也跟着移植的过程,在移植的过程中,可以学 docker-compose 的用法,以及跨架构打包 docker image ,还有单点登录的集成。这些都是我的兴趣点,而且也想学一学里面设计的机制,大学毕业的时候我想设计一款 NAS,那也仅仅是基于 centos 做了一些改动,后来买了威联通,虽然不常开案例问问题,但问的也仅仅是关于这个产品本身的东西,包括专业程度和响应级别都不是能够一概而论的。甚至连 trouble shooting 上传日志都很方便。
全容器化的服务以及对操作系统的修改,可以看出沿袭了当年在 deepin 的风骨。包括应用商店在内,很多系统组件都完全采用用容器托管。还有自己的单点登录系统,而对于 OIDC 的支持其实很多企业都没有做到。还有一点不得不提的,开发者的社区很活跃(主要指的是微信群和上架应用商店),每天都会有几位开发人员默默的上架应用和攻略。慢慢的我也熟悉了把 docker images 转换成为懒猫商店的模式。也上架了自己的几个应用,有原创的,也有把喜欢的开源项目移植过来。
最喜欢的原生应用是网盘和清单,首先说网盘曾经有一篇为什么没有人去做网盘的帖子,讲述了网盘研发成本高,就连曾经宣称用不限速的阿里云盘也变节了。改善 NAS 生态是刀山火海、暗礁遍布,却仍要做那一股清流;研发与售后成本明摆在前,却仍坚持全线自研,把服务做到极致。清单有种小清新的感觉,极简风格,日常记录一些 todo,主要同步之后多平台编辑实在很舒服。打破了关于以前产品自带的软件都很烂的固有观念。
想起来《琅琊榜》中的一句话拿来形容创始人,“如此愚蠢,却又如此有胆识的人,已经很久没见到了。”
我一直相信技术是服务生活的,但慢慢的变成了炫技以及慢慢变成了改需求以及最后变成了漫长的牛马生涯,在学生时代一直有一个远景,做一款全平台的软件自己用,后来发现学习成本巨大,而且也没有资金外包出去,虽然这几年接触了 flutter,但也没有构建一个全新的跨平台产品出来。
有了懒猫微服之后,这一切都解决了,只需要打包好 docker image,如果可以的话就上架商店给其他人用。用公网访问,TLS 证书卸载这些都一步搞定。当我们默默的喷绿联,极空间丢数据,群晖如何守旧不肯升级 CPU,以及限制磁盘认证的问题。曾经我们还忽略了这样一款从操作系统,软件生态,甚至应用商店。这款机器比我之前 DIY 构想的还要完善,完美。
我想去拆解他的技术细节。但是不想再出来一个商业竞品来扰乱这份宁静。在我看来这是一款充满着技术者热情和情怀的产品。
附送:懒猫微服社区激励机制一览
贡献类别
具体动作
奖励金额
备注条件
应用移植
成功将一款高质量的自托管应用移植并上架商店
100 元/款
- 必须功能正常- 开源应用需标注上游作者- 若多人移植同一应用,仅首位上架者得奖
对接账户系统 / 网盘右键菜单
在移植基础上完成接口对接
+50 元/款
- 自己移植并对接:共 150 元/款- Fork 他人应用并补充对接:50 元/款
应用攻略编写
发布含截图且经验证可行的攻略,并关联商店应用
50 元/篇
鼓励分享使用经验,惠及社区
不予奖励的应用类型
纯网页游戏、离线 Web App、纯数据库软件等
——
可自由上传,但暂无红包激励
核心要点
先到先得:同款应用仅首位合规上架者获奖。
质量至上:功能正常、信息完整方可审核通过。
额外加成:完成账户系统/右键菜单对接可叠加奖励。
知识共享:高质量攻略同样有奖,鼓励经验传播。
社区激励机制:https://developer.lazycat.cloud/store-rule.html
懒猫打金服:https://playground.lazycat.cloud/#/guideline/448
---
## 懒猫微服实战入门(五):文件上传到懒猫网盘,SMB 电视盒子观影
- URL: https://blog.no-claw.com/posts/b62d86a5/
- 日期: 2025-05-14
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服, Samba
作为一个合格的 NAS,肯定要有文件共享的功能,一般我们常用的是 SMB,NFS 和 WebDav 这三种,然后需要设置共享目录和用户权限。
懒猫网盘提供了一个开箱即用的方案,直接通过 APP 把网盘的文件夹映射自动挂载到本地,不需要像 Linux 那样 mount,也不需像 window 一样新建磁盘映射:
我们看看以前要挂载一个盘有多麻烦:
1
2
3
4
5
# Debian/Ubuntu
sudo apt install cifs-utils
sudo mkdir /mnt/smb_share
sudo mount -t cifs //SERVER_IP_OR_NAME/SHARE_NAME /mnt/smb_share -o username=SMB_USER,password=SMB_PASSWORD,domain=WORKGROUP
如果需要开机自动挂载,还得改/etc/fstab 里面的条目。但是,懒猫网盘可以开箱即用,不管你是用浏览器,APP,还是用访达挂载 SMB 都访问都可以。属实是解放了 Mac 党的电脑空间。
在网盘中点击自己的头像,然后设置 - 网络服务这里,可以看到设置。甚至点击起开内网服务,还会给一个 IP 地址的 SMB 地址:
smb://ip/user-name,电视盒子不能安装懒猫 app,但是有了 IP 地址之后就可以连接 SMB 了~
然后就是当贝盒子这里啦,如果你是小米盒子或者其他的盒子,只要文件管理器支持 SMB 就 OK
进入文件管理器,选择 局域网共享连接。
然后输入懒猫微服的 IP 地址,用户名密码就是微服 APP 的, 这一套有点 AD 域的感觉了。
如果你的文件管理器默认没有 SMB 也没有关系,还可以使用第三方应用进行 SMB 连接,比如这个 Github 项目,可以从 release 中下载 APK 进行安装。
连接成功后,可对文件进行扫描和管理。
通过以上配置,就可以在电视盒子上通过 SMB 连接 NAS,开心的观看的 4K 电影了。
---
## 懒猫微服进阶心得(一):M芯片移植懒猫应用构建Docker镜像的常见问题排查及解决方案
- URL: https://blog.no-claw.com/posts/126ed22c/
- 日期: 2025-05-13
- 分类: 懒猫微服 / 进阶
- 标签: Docker, 懒猫微服
本文是使用 Apple silicon 的 MacOS 移植到懒猫商店的踩坑记录,希望能够给大家带来帮助
Apple silicon 很好,在这年几乎带起来 ARM 的生态。但这也拉开了 ARM 和 X86 之战,用户在两大生态中穿梭,只能增加自己应用的兼容性。就比如说用来打包的 Docker image,尽管编程语言和操作系统都在底层屏蔽了硬件架构,但是容器还得用相同架构的。
这是之前移植开源项目时候忘记打包不同架构的 image 而直接推送到懒猫镜像仓库导致的问题。MacOS 默认打包了 ARMv8 架构的镜像,在 X86 上也无法运行。
1
2
pg-docker run -p 5000:5500 registry.lazycat.cloud/u04123229/you/doudizhu-scorer:d1d9085174c0bf8c
WARNING: The requested image's platform (linux/arm64/v8) does not match the detected host platform (linux/amd64/v4) and no specific platform was requested
由于打包的时候容器一直在反复重启,所以在 dozzle 上也没有什么明显的报错,所以有个办法就是 ssh 进去用终端pg-docker run,这样所见即所得。dozzle 地址:https://dev..heiyu.space/dozzle/
于是重新打包,跨架构打包时候需要使用 buildx,当然前提是需要里面的运行时和代码也是跨平台的。
我们先来看概念和原理:
buildx 是 Docker 提供的一种扩展功能,它基于 BuildKit 引擎,目的是为 Docker 提供更强大的构建功能,包括:
跨平台构建:支持在一种平台上构建适用于多种平台的 Docker 镜像。
缓存管理:支持高效的缓存管理机制,能够减少重复构建的时间。
多阶段构建:支持复杂的多阶段构建流程。
buildx 使 Docker 能够生成多平台的镜像,这意味着你可以在一个平台上(例如 ARM 或 x86)构建适用于其他平台(如 x86_64、arm64、armv7 等)的 Docker 镜像。
docker buildx build 通过指定 --platform 参数来告诉 Docker 在构建时要生成哪些平台的镜像。例如,linux/amd64 和 linux/arm64 就分别对应 x86 和 ARM 架构。
在 buildx 构建完成后,你得到的不是一个单独的镜像,而是一个支持多平台的 manifest list,这个列表包含了不同架构的镜像。这个列表可以推送到 Docker Hub 等镜像仓库,客户端在拉取时,会根据自己的硬件架构自动选择合适的镜像。
这意味着,我们可以通过同一个镜像标签(如 your_image_name)来支持多个平台的 Docker 镜像,而用户在拉取时会自动选择适合自己平台的镜像。
准备构建环境:
Docker Buildx 会首先准备并选择一个构建器(builder)。这个构建器负责在指定的平台上执行构建任务。
选择平台:
使用 --platform 参数来选择目标平台,Docker 会通过 QEMU 模拟器或者本地平台来执行构建。
构建镜像:
在选择平台后,Buildx 会根据 Dockerfile 和其他构建上下文开始构建镜像。它会处理平台特定的依赖和构建步骤。
生成适配镜像:
对于每个平台,Docker Buildx 会生成一个特定的镜像。例如,对于 linux/amd64 和 linux/arm64,它会分别为这两个平台构建独立的镜像,并将它们绑定在一个 manifest list 中。
推送镜像:
完成构建后,你可以使用 --push 参数将包含多个架构镜像的 manifest list 推送到 Docker Hub 或其他镜像仓库。这个清单包含了多个平台的镜像,当用户从仓库拉取时,Docker 会自动选择与用户当前平台兼容的镜像
然后来实操
确保 Docker 版本支持 Buildx,用docker buildx version 来验证/
创建并使用新的 Builder
打包的时候加上平台参数:--platform linux/amd64,linux/arm64
具体命令如下:
1
2
3
4
docker buildx version
docker buildx create --use --name multiarch-builder
docker buildx build --platform linux/amd64 -t your_image_name .
docker buildx build --platform linux/amd64 -t your_dockerhub_username/your_image_name --push .
在这过程中,我们可能还会使用docker tag ,这个命令可以将一个现有的镜像打上新的标签(tag),通常用于将镜像标记为自己的名字或指定版本。这对于推送镜像到 Docker Hub 或其他镜像仓库时非常有用。
1
docker tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]
**SOURCE_IMAGE[:TAG]**:要打标签的源镜像。TAG 是可选的,如果不指定,默认是 latest。
**TARGET_IMAGE[:TAG]**:新的目标标签,通常你可以为镜像指定一个新的名字或版本号。
假设你有一个名为 my_image:latest 的镜像,并且你希望将它标记为属于你自己(例如,your_dockerhub_username/my_image:latest):
1
docker tag my_image:latest your_dockerhub_username/my_image:latest
这条命令会将 my_image:latest 镜像打上 your_dockerhub_username/my_image:latest 的标签。
推送到 dockerhub 之后,然后就可以像往常一样使用 Docker 了。
1
2
docker pull your_dockerhub_username/your_image_name
docker run your_dockerhub_username/your_image_name
如果想走 Github action 一键打包 image 的话,是这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
name: Build and Push Docker Image
on:
push:
tags:
- "v*" # 仅在 tag push(如 v1.0.0)时触发
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Check DockerHub secrets
run: |
if [ -z "${{ secrets.DOCKER_USERNAME }}" ] || [ -z "${{ secrets.DOCKER_PASSWORD }}" ]; then
echo "❌ ERROR: DOCKER_USERNAME or DOCKER_PASSWORD is missing"
exit 1
fi
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
install: true # ✅ 自动创建默认 builder
- name: Docker login
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract tag name
id: vars
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Build and push Docker image (multi-arch + latest)
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: |
cloudsmithy/flask-demo:${{ env.TAG }}
cloudsmithy/flask-demo:latest
没有把 lzc-cli 写进去的原因是目前只能从终端命令行查看到推送到懒猫仓库的镜像命令,目前还不能存到一个中间位置,所以做了一个通用的版本。
事情到这里本来应该结束的,但似乎有了新的故事。
故事 1:无法打包
某次在打包的过程种突然报错,期间一度以为 Orbstack 出现了问题,于是卸载重装,重启电脑,均无效,GPT 和 deepseek 也只是让我检查网络连接。期间重新 docker pull 也是没问题的。
1
characters ERROR: failed to dial qRPC: rpc error:code = Internal desc = rpc error: code = Internal desC = header key "x-docker-expoSe-session-name" contains value with non-printable ASCI #2793
无奈只能 Google,在 issue 里有一个评论,打包目录不能出现中文。(我的 OS 默认中文) 参考链接:https://github.com/docker/buildx/issues/2793
故事 2:构建之后没有输出
docker buildx build --platform linux/amd64 -t your_image_name .
1
2
WARNING: No output specified with docker-container driver. Build result will only remain in the build cache. To push result image into registry use --push or to load image into docker use --load
这是使用 docker buildx 构建镜像时,指定了 docker-container 驱动,但是没有使用 --push 或 --load 参数。结果是,构建的镜像只会保留在构建缓存中,而不会被推送到镜像仓库或加载到本地 Docker 环境中。
我们可以通过两种方式之一来明确指定输出目标,避免出现此警告:
1. 使用 --push 将镜像推送到远程仓库:
如果你希望构建的镜像推送到 Docker Hub 或其他 Docker 镜像仓库,可以使用 --push 参数。例如:
1
docker buildx build --platform linux/amd64 -t your_image_name --push .
这将把镜像推送到 Docker 仓库,而不是仅保留在本地构建缓存中。
2. 使用 --load 将镜像加载到本地 Docker 环境:
如果你想将构建的镜像加载到本地 Docker 环境中以便后续使用(例如运行容器),可以使用 --load 参数:
1
docker buildx build --platform linux/amd64 -t your_image_name --load .
这会将构建的镜像加载到本地 Docker 环境,使你可以在本地运行、调试或进行其他操作。
故事 3:无法同时保存双平台 Image 到本地
1
2
3
docker buildx build --platform linux/amd64,linux/arm64 -t cloudsmithy/shuangpin:latest . --load
[+] Building 0.0s (0/0) docker-container:stoic_hellman
ERROR: docker exporter does not currently support exporting manifest lists
--load 只适用于单平台构建。如果你在跨平台构建(如 linux/amd64,linux/arm64)时使用 --load,则只会将构建的默认平台镜像加载到本地,不会加载所有平台的镜像。跨平台构建时,通常需要使用 --push 将所有平台的镜像推送到远程仓库
1
docker buildx build --platform linux/amd64 -t cloudsmithy/shuangpin:latest . --load
使用 --load 时,镜像会被加载到本地 Docker 守护进程中。对于大镜像,加载过程可能需要较长的时间和较多的本地存储空间。因此,如果镜像非常大,可能需要考虑是否使用 --push 直接推送到远程仓库,而不是将其加载到本地。
1
docker buildx build --platform linux/amd64,linux/arm64 -t cloudsmithy/shuangpin:latest . --push
故事 3:懒猫仓库黑魔法
对了,关于文档上提到的懒猫的 registry 不能在微服外面用,黑魔法的限制其实就是加了认证,直接返回 401.
1
2
3
docker run -p 5000:5500 registry.lazycat.cloud/u04123229/you/doudizhu-scorer:d1d9085174c0bf8c
Unable to find image 'registry.lazycat.cloud/u04123229/you/doudizhu-scorer:d1d9085174c0bf8c' locally
docker: Error response from daemon: Head "https://registry.lazycat.cloud/v2/u04123229/you/doudizhu-scorer/manifests/d1d9085174c0bf8c": no basic auth credentials.
最后
希望这篇文章能够帮助大家在将开源项目移植到懒猫商店时,避免遇到一些常见的坑和问题。祝大家顺利解决跨平台构建和镜像推送中的挑战,提升开发效率!
---
## 懒猫微服实战入门(四):完全DNS访问,和本地代理冲突了怎么办?
- URL: https://blog.no-claw.com/posts/36782d5f/
- 日期: 2025-05-12
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服, 代理
刚拿到懒猫微服的时候,了解到这个机器完全使用 DNS 来访问是很吃惊的。拒不完全使用经验,大概是机器里部署了一套私有的 DNS server,然后广播到整个局域网。而公网上的则是 heiyu.space,通过 whois 查看,公网的 domain 是在腾讯云购买的。
所以应该是两套的解析结构,局域网访问的时候,就先用机器部署的私有 domain 进行解析,如果使用流量或者在外边,就是走互联网上 DNSPod 的解析记录。这个结论属于猜测,因为很多公有云也确实四这么做的,一个公开托管的 domain 用来互联网解析,一个 VPC 内的 private domain 用来解析 VPC 内部的地址。
懒猫微服和传统的 NAS 又很大的不同,如果作为小白玩家可以很快上手,当做 Sass 服务来用。但对于专业玩家,总有一种技术的强迫症,总用抽丝剥茧,从 Saas 一点点解析到 Iass,然后一点把懒猫编程能够公开访问的私有云。
比如网络。可以通过 dig 或者 nslookup 来解析
1
2
dig xxx.heiyu.space +short
dig xxx.heiyu.space AAAA +short
但是,DNS 解析这里慢慢就出现问题了。在某次上传文件到懒猫网盘的时候,我发现速度慢的可怜,几乎是走了公网。在 VIP 答疑群里得知,流量应该是从代理转了一圈,然后回来的,所以慢,剩下的就是解决这个问题了。
那么办法就是放行白名单,不让他走代理,由于是 DNS 访问,而很多代理的规则是根据域名匹配的,所以要去改这个匹配规则。当然如果你用 nmtui 配置静态 IP 地址的话,那么内网访问也是没有问题了,直接走上级路由的默认路由表即可。
而白名单主要是放行, _.heiyu.space 和 _.lazycat.cloud 这两个域名,heiyu.space 是穿透服务,lazycat.cloud 是官网和论坛。
不同的软件有不同的设置办法,比如说用 DOMAIN-SUFFIX 来替代域名的泛解析,所以放行的时候 heiyu.space 这这样子就好。我在修改配置文件的时候用 DOMAIN-SUFFIX 匹配*.heiyu.space 不生效,花了不少的时间。实际不需要再写一次 * 号。
而最终落到配置文件上就是这样的。(之前写 DOMAIN-SUFFIX,*.lazycat.cloud,DIRECT)一直不生效。
1
2
3
4
rules:
- DOMAIN-SUFFIX,lazycat.cloud,DIRECT
- DOMAIN-SUFFIX,heiyu.space,DIRECT
- DOMAIN-SUFFIX,deepseek.com,DIRECT
也总结一下其他规则吧,最常见的类型有这些:
1)DOMAIN
只匹配某个域名本身。
举例:gs.apple.com → 只有访问 gs.apple.com 才会命中。
2)DOMAIN-SUFFIX
匹配所有以这个后缀结尾的域名。
举例:apple.com → gs.apple.com、itunes.apple.com 都会命中。
3)DOMAIN-KEYWORD
匹配包含某个关键词的所有域名。
举例:apple → apple.com、gs.apple.com、appleabc.xyz 都会命中。
4)IP-CIDR
匹配某个 IP 地址段。
举例:192.168.0.0/16 → 匹配 192.168 开头的所有 IP。
这些是规则写法里最基本的几种,掌握了就能应对绝大多数情况。
---
## 懒猫微服实战入门(三):懒猫智慧屏,以为是地表最强电视盒子,结果是闺蜜机
- URL: https://blog.no-claw.com/posts/5a9099c0/
- 日期: 2025-05-11
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服, 电视盒子
过年那阵子,为了看春晚不卡顿,打算换一个当贝盒子,尽管这些年刷过矿机当盒子或者 Armbian,但是仍然对当贝系列情有独钟,而时过境迁,最后发现在当年风靡市场的网红盒子,不过 RK3566 而已,根据不同的 RAM+ROM 的组合来卖到更高的价格,实在是没有什么性价比。
于是想到 X86 能性能会更加好一些,比如 Android TV 这种固件,不过这种基本都是海外版本,纯净的基本什么都要自己捣鼓,而 B 站评论的第三方链接又很担心安全问题。
这问题一直持续到我购买了懒猫微服,高性能的 X86 主机,还带 HDMI(目前 Typec 不支持供电和视频传输),和飞牛的原生 Gnome 输出不同的是懒猫智慧屏其实是商店里面一个独立的 APP,本质上相当于客户端,需要手机扫码授权才能使用
登录之后是这样的,其实就是在原来 APP 的基础上加了一个 Chrome 浏览器,然后其他的应用也能在显示器上打开。右上角依次是搜索栏,软件商店和登出按钮。接下来就是可以愉快的观影了,个人体验新开一个账户,然后设置应用白名单体验会更加的好~(但是用手机遥控的时候会提示,智慧屏正在被其他用户使用是否停止 hhhh)
我尝试的观影流程如下:
把视频传输到懒猫网盘(拖拽上传或者 Samba)
打开视频播放器(其他播放器也可)
然后选中网盘文件就可以播放啦
目前支持手机端遥控,类似市面上的盒子助手, 我连接了 Action III ,能够愉快的观影了。
由于是内置的应用,所以不存在电视盒子广告乱象的问题,11 代 I5 拿来观看 4K 也是轻松秒杀,需要什么就往网盘传什么。高端的需求只需要简单的办法。感觉不用买 Apple TV 了,如果需要其他软件比如 jellyfin 啥的也可以自己部署~。
机器有 USB 还能插键鼠,接好外设的时候记得重启一下,应该是容器部署的原因,让容器在启动的时候再读一遍/dev/bus/usb 下的设备。除了观影,接会议室的演示大屏幕也不错~
毕竟这个页面,连 debian 虚拟机都能使用,其他的应用也不在话下。想用 X86 安卓的话,直接安装到虚拟机里~
为啥说可以成为闺蜜机呢?
为啥说可以成为闺蜜机呢?
某天在商场的时候看到一个大大的带支架的还可以随时移动的平板叫做闺蜜机。
那么懒猫微服 + HDMI 投屏器/毫米波投屏器 + 可移动支架,是不是很像?
某宝上还有这样的改装套餐,解决显示器供电看来就 OK。
市面上没有好用的电视盒子?选懒猫!
市面上没有良心的在线网盘?选懒猫!
市面上没有便宜的闺蜜专机?选懒猫!
市面上的闺蜜机动辄大几千,配置又差强人意,还不如买懒猫微服,还有专业的售后陪玩,探索无限可能!
什么?你说闺蜜机还得有语音助手, AI 美颜,娱乐互动?快登懒猫微服用 Docker 来部署吧,还有机会上架懒猫商店哦~
---
## 车机锁 Wi‑Fi 奸商跑路,高楼组 Mesh 极客破局
- URL: https://blog.no-claw.com/posts/fc9b2cf2/
- 日期: 2025-05-10
- 分类: 电子产品 / 路由器
- 标签: 车载
五一假期的时候家里人打算自驾游,才发现车载地图已经很多年没更新了,找不到要去的地方,于是打算重新弄下,来一个全新的体验。
车载的导航是安卓,虽然已经很久没更新过了。从早些年折腾刷机的经验来看,就算不能连接 wifi,也应该是能够用 USB 转接的。尝试一圈,基本是这个情况,流量卡失效,wifi 功能被禁用掉,USB 转接有线网卡也无法识别。
于是打电话到 4S 店询问是否能够提供些许的支持,他们转到技术,然后告诉只能把车开到他们那里去看,不提供上门的支持。而作为这个年代的资深消费者,已经有千万遍劳心费力的折腾最后被售后三两句打发走的经历。 好说歹说发一张图片过去,尽管对方语气中透着些许的不耐烦。“你这个是赠送的,我们不了解情况,不是我们原厂的东西,而且很多车机是无法升级的”。于是又开始推销了自己的产品,问到是否还会遇到上述情况,还得 case by case 来看。“你这个车已经买了很多年了,早就没有保修了。”
我的需求无外服这三种:
更新高德地图数据,不管是更新版本还是导入离线包。
升级 carplay,需要买一个盒子,还需要做安装 Autokit 硬件检测,最快也要明天到。
刷一个有 wifi 功能的正常版本,让我正常更新数据。(最后也没实现,不过估计新版本更卡)
朋友帮我查了下这个版本的卡槽,需要把整个导航拆下来,不过弄不好还耽误用倒车影像,遂放弃。
在这个论坛:http://www.allmost.org/2019/11/android-head-unit-root-device-model.html
糟糕的高德地图
无法联网更新,首先想到的是离线数据包,于是致电400-810-0080,这次都是机器人接听,无法转人工。
然后根据提示给了我这样的一条信息。
1
【高德地图】尊敬的高德用户您好,您咨询的地图数据升级问题,操作方法:请您点击链接 https://auto.amap.com/download/map_data#public ,选择对应版本下载即可;同时您可在下载界面点击我不会安装查看说明, 感谢您对高德的关注与支持!
在年初的时候还针对 Mac 端的高德地图无法更新的问题咨询客服,却被告知没有这样的产品进而“建议”使用网页版。,而我一直是在 App Store 下载的。当时几乎想到距离我两三公里的高德总部去要个说法,而这次的离线数据包更是提示升级中,无法下载。
于是退而求其次,我下载一个最新的机车版是不是会更好一些?安装之前还担心数据丢失问题,于是看到下面这个话就“放心”了。
但是安装之后,之前的离线地图全部消失,甚至很”贴心“的把之前旧数据的磁盘空间都释放了出来。原来马上爆满的磁盘空间,突然瘦身了。但,实时导航,手机同步,问题反馈都共用报错无法联网,这次而且无法回滚。不能上网的车机,不能联网的地图,被清空的旧版数据。第二次想杀到高德总部。
ADB 的奇迹
大学同学在嵌入式行业,经验比我丰富些,于是去求助。他告诉我可以尝试通过 ADB 进入调试系统,然后使用命令行拉起来 wifi 的进程 ,看看是否能够改善。
分析之后查看到,车机的版本是 Android6.0,基于 Linux3.18 的内核,连包管理工具也没有,甚至连 top 都会把上位机卡死。于是开始分析思路,能够开启热点说明有 wifi 模块,然后依次排查进程是否启动,驱动是否安装,内核在启动时是否正常加载该驱动。那一刹那,感觉自己从调侃的修电脑,修家电,修水电,最后到了修车侠。
还好支持开发者模式,adb 调试的时候使用了甲壳虫助手,比用 USB 连接安卓 ADB 方便的多,把软件安装在车机上,不用再像以前刷机一样一遍又一遍的执行 adb device -l 查找设备,虽然这个机器慢一点,但是总归还是能够很稳的连接 ABD,然后一遍一遍尝试命令,Google,GPT,Deepseek,一遍又一遍。
机车给了一个 USB 的接口,刚好拿来外接键盘,把 MBP 搬到车里查资料,手机开了热点越来越烫,然后就是一遍的调试,而 MBP 不能通过 adb 扫描到机车,不确定是不是和工厂模式里的设置有关系。这过程有点像刷机,有一点想像服务器上调 Linux。在此之前也想过用 USB 转 J45 连接路由器,就算没有网络的情况下,也应该能够显示一张没有网络连接的以太网卡。试验之后是完美没有,和朋友讨论之后,猜想是和内核驱动在编译的时候没有打包通用驱动或者这张卡驱动导致的。(来自之前 UFS 安装 Linux 的经验)而这个机器上完全无法执行 lsmod 和和 cat /proc/modules,而**dmesg | grep -i "load"**的记录也是空,实在无法想象这个系统是如何加载底层驱动的。
最后思来想去大概是这几个命令生效了,因为后来重启才发现 wifi 模块加载出来了,中间加执行了很多。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 在甲壳虫中可以省略adb shell字眼
# 1. 直接指定组件启动 wifi(好像是自动进入 WLAN 搜索页面)
am start -n com.android.settings/.wifi.WifiSettings
# 2. 用系统通用 Intent(推荐)
am start -a android.settings.WIFI_SETTINGS
# 开启 Wi‑Fi
adb shell settings put global wifi_on 1
# 立即让系统刷新一次广播(有的 ROM 必须)
adb shell am broadcast -a android.intent.action.WIFI_STATE_CHANGED --ez state true
验证:
adb shell settings get global wifi_on → 返回 1;
adb shell dumpsys wifi | grep "Wi-Fi is" → 出现 enabled
回去看到 GPT 的提示,说操作完一定重启,否则配置不会写入 NVRAM。(我之前忽略了这个,走了很多弯路)
GPT 这些内容帮助很大,可以用来验证:
我的dumpsys wifi 里出现的那一行长这样,
1
Wi‑Fi is disabled
它只说明 系统目前把 Wi‑Fi 开关拨到了“关”,并不直接告诉你“有没有 Wi‑Fi 硬件”。要分清楚是“硬件存在但关掉”还是“根本没有硬件/驱动”,可以再看 4 处信号:
| 检查点 | 如何查看 | 典型输出 | 结论 |
| ——————- | ————————————– | ———————– | —————————————————- | ———————————————- | — |
| 1. Service 列表 | adb shell service list | grep -i wifi | wifi: [android.net.wifi.IWifiManager] | 看到这一行 → Framework 里 有 Wi‑Fi 服务 |
| 2. 驱动状态 | adb shell getprop wlan.driver.status | ok / ready / unloaded | 有值 → 内核认识 Wi‑Fi 芯片;空值 → 多半缺驱动/被裁掉 |
| 3. 网络接口 | adb shell ifconfig -a | grep wlan | wlan0 ... | 能看到 wlan0 → 芯片已初始化;完全没有 → 未加载 |
| 4. 内核模块 | adb shell lsmod \| grep -E "8188 | rt5370 | wlan" | | |
结果判读
情况
解释
解决办法
Service 有,wlan0 有,driver=ok,但 dumpsys 显示 disabled
硬件存在,只是软件开关关掉
用 UI 开关或adb shell settings put global wifi_on 1再重启 Wi‑Fi
Service 有,但 wlan0/驱动都没有
Framework 在,但 内核缺驱动/Factory 里关闭了 Wi‑Fi
先在工厂设置里勾选 WIFI 并保存;如仍无驱动 → 刷带 wifi_bt 的 ROM 或插 USB 网卡
Service 没有(grep 不到 wifi)
ROM/MCU 层完全裁掉了 Wi‑Fi
只能刷完整固件或硬改/外接网卡
Service 有,driver=unloaded
驱动文件在,但未成功加载
Factory 里未启用 / 模块版本不匹配 → 勾选保存或换匹配的系统+MCU
快速打开 Wi‑Fi(无 svc 的情况下)
1
2
3
4
adb shell settings put global wifi_on 1
adb shell am broadcast -a android.intent.action.WIFI_STATE_CHANGED --ez state true
# 然后再查看
adb shell dumpsys wifi | grep "Wi‑Fi is"
如果改完仍显示 disabled,就说明硬件/驱动层面还没点亮,需要回“工厂设置”勾选 Wi‑Fi 或换带驱动的固件。
一句话:dumpsys wifi 看到 disabled 只是“当前关着”,并不代表“没有 Wi‑Fi”。再看服务列表、驱动状态、wlan0 接口等就能分辨:
都在 → 只是没开;
驱动/接口缺失 → 硬件被关闭或驱动裁掉,需要在工厂设置启用或刷新固件。
我的结果是。无法执行 lsmod,服务正常,驱动正常,而网络接口必须在启动共享热点的时候才能看到,最后在工厂模式的时候手滑点了重启才发现一切正常。
看到这个页面的时候就感觉一切的付出都值得了,所谓念念不忘,必有回想吧。(设置里还是没有 wifi 选项,还得自己从下拉菜单启动)
工厂模式:
主要用来开关一些功能,实际在我这个过程中好像没有起到太大的作用,这个主要针对整个 OS 而言,有些版本可能在这里启动 wifi,但是我这个没有,主要还是见识一下车机模式,图一乐。
这些是 GPT 给的,没有真正执行:
ADB 直接拉起
1
2
3
4
5
6
bash
复制编辑
adb shell am start -n com.twd.settings/.FactorySetting # FYT/TS10 系常见
adb shell am start -n com.android.settings/.DeviceInfoFactory # 某些 XY‑Auto
若提示 Error type 3 仍找不到,说明 ROM 确实删掉了对应 Activity。
Mesh:高层 WIFI 的荣耀
家里在高层,即使把车开到楼下也不见能够有多好的信号,也能是根本就搜不到。然后突发奇想,正好之前有一个
交流的移动电源,然后把 Mesh 子路由拿到楼下供电,然后把楼上的路由放在窗户上。这才是有线 Mesh 的正确用法,尽管在高层,还能跑到 10M 以上的速度,然后再用楼下的子路由无线桥接,可以媲美一些无线路由器的速度了。
其实还有一个方案,借助 POE 路由的便携性,把网线从窗户顺下去,然后顺便也解决供电的问题。但是,翻了翻箱子,确实没有这么长的网线,那么就速战速决,将就一下把。
设备如下,方案做好了,摸黑也要实施完。
这过程中的命令和帖子
一些安卓 8 以上才可以用的命令,先记录下来,作为折腾的过程。
1
2
3
4
5
6
7
8
9
iwlist wlan0 scan
adb shell am broadcast -a android.intent.action.WIFI_STATE_CHANGED --ez state true
adb shell am start -n com.android.settings/.wifi.WifiSettings
https://blog.csdn.net/BOJUE01/article/details/136684847
https://blog.csdn.net/BOJUE01/article/details/136684847
https://www.cnblogs.com/linux-37ge/p/11111596.html
https://blog.csdn.net/pwp032984/article/details/126286159
http://www.allmost.org/2019/11/android-head-unit-root-device-model.html
以下来自 GPT,也帮了很大的忙
下面给你 4 种常用办法,按「年代新 → 旧」排列,只要你的 ROM 里有对应的命令或工具,就能在 PC 端终端 直接看到扫描结果。不用担心全记不住,逐个试,命令不认识就跳下一个。
方法
适用 Android 版本
是否要 root
命令示例
A. cmd wifi 子命令
8.1 (Oreo) 及以上,只要系统内置 cmd
❌
bash\nadb shell cmd wifi enable # 先确保开 Wi‑Fi\nadb shell cmd wifi scan # 立即触发一次扫描\nadb shell cmd wifi list-scan-results # 打印扫描结果(BSSID/SSID/信号)\n
B. dumpsys wifi 解析
4.0+(几乎所有)
❌
```bash\nadb shell dumpsys wifi
C. wpa_cli 工具链
ROM 里带 wpa_supplicant + wpa_cli
⭕(通常要 root)
bash\nadb shell su -c 'wpa_cli scan'\nadb shell su -c 'wpa_cli scan_results'\n
D. iw/iwlist 直查射频
内核自带 mac80211/CFG80211,工具在 /system/xbin
⭕
```bash\nadb shell su -c ‘iw dev wlan0 scan
步骤拆解(通用)
让 ADB 连上车机
1
adb devices # 出现 device 才算连上
确保 Wi‑Fi 已经启用
可用 adb shell cmd wifi enable 或 settings put global wifi_on 1 后再开一次 UI 开关。
查看状态:adb shell dumpsys wifi | grep "Wi‑Fi is" → enabled/disabled。
触发一次扫描
新版用 cmd wifi scan;旧版可发送广播:
1
adb shell am broadcast -a android.intent.action.SCAN_RESULTS
查看结果
任选上表 4 种方法里你设备支持的那一行。
若看不到 SSID,只见一堆 :说明热点隐藏了 SSID,属正常现象。
命令输出示例(cmd wifi list-scan-results)
1
2
3
4
BSSID frequency level SSID
60:45:cb:aa:1b:30 2462 -55 HomeWiFi_2G
60:45:cb:aa:1b:34 5200 -70 HomeWiFi_5G
b0:d5:9d:cc:31:c0 2412 -65 MiPhoneHotspot
字段含义:
字段
解释
BSSID
无线 AP 的 MAC 地址
frequency
2412 – 2472 MHz = 2.4 GHz;5180 + = 5 GHz
level
信号强度,单位 dBm(-30≈ 满格,-80≈ 临界)
SSID
热点名称(隐藏网络会显示 )
常见坑 & 对策
症状
原因
解决
cmd: not found
ROM 删了 toybox / cmd
改用 方法 B、C、D
cmd wifi: Unknown command
Android 7.1 及以下
用 dumpsys 或 wpa_cli
permission denied / su: not found
没 root
选不用 root 的 方法 A 或 B
扫描结果为空
Wi‑Fi 实际没启用、没有天线、驱动缺失
先 settings put global wifi_on 1 并从 UI 打开;或检查硬件/ROM
小贴士
批量重扫:想每 5 秒自动扫一次可用循环:
1
while true; do adb shell cmd wifi scan; adb shell cmd wifi list-scan-results | head -n 10; sleep 5; done
只取最强信号:
1
adb shell cmd wifi list-scan-results | sed 1d | sort -k3 -n | head
无 Wi‑Fi 硬件时:任何扫描命令都会报错或空结果 —— 确认已在 Factory Setting 勾选 Wi‑Fi、或插上 RTL8188EU/RT5370 USB 网卡后重试。
一句话:连好 ADB → 开 Wi‑Fi → 在 PC 端执行 **cmd wifi scan && cmd wifi list-scan-results**(Android 8.1+ 最方便);旧版系统就用 adb shell dumpsys wifi 或 root 下的 wpa_cli scan_results / iw dev wlan0 scan 来列出热点。祝你早日看到满屏 SSID!
dumpsys 是 只读(dump) 的诊断工具——它只能把系统当前状态“倒”出来给你看,完全没有写入/控制能力。
所以:
dumpsys wifi 只能查看 Wi‑Fi 的开关状态、扫描结果、连接信息等,不能直接把开关从 OFF 变成 ON。
为什么 dumpsys 不能改状态?
实现方式:dumpsys 调用各系统服务的 dump() 接口,这个接口按约定必须是“无副作用”的纯输出。
权限限制:即使给 dumpsys root 权限,它也只有 READ 能力。
系统设计:修改 Wi‑Fi 状态属于 IWifiManager.setWifiEnabled() 这一类“更改设备行为”的 API,必须走 Binder 或写全局设置值,不能通过 dump。
不用 UI 打开 Wi‑Fi 的正规方法(回顾)
任选其一,具体支持情况取决于你的 ROM / 是否 root
方法
是否 root
Android 版本
示例
settings put global wifi_on 1
不要求
4.0+
adb shell settings put global wifi_on 1
service call wifi 13 i32 1
需要
4.1+
adb shell su -c 'service call wifi 13 i32 1'
cmd wifi enable
不要求
8.1+
adb shell cmd wifi enable
广播/Intent(部分 ROM 有效)
不要求
4.0+
adb shell am start -a android.settings.WIFI_SETTINGS(拉起设置页后系统会自动打开)
svc wifi enable 也是写操作,但你的 ROM 把 svc 精简掉了,所以用不了。
一键脚本(无 root 情况下最通用)
1
2
3
4
5
6
# 1. 打开 Wi‑Fi(写 settings)
adb shell settings put global wifi_on 1
adb shell settings put system wifi_on 1
# 2. 触发系统刷新(广播)
adb shell am broadcast -a android.intent.action.WIFI_STATE_CHANGED --ez state true
大多数 MT3561 Android 6/7 车机用这两行就能把 Wi‑Fi 开关拨到 ON。
如果执行后 dumpsys wifi 仍显示 disabled,说明:
ROM 在启动脚本里强制关掉 —— 去“工厂设置”里把 WIFI 复选框勾上并保存;
硬件或驱动缺失 —— 需要刷带 Wi‑Fi 的完整 ROM,或外接 RTL8188EU/RT5370 免驱 USB 网卡。
结论
dumpsys wifi 只能看,不能改。
想无 UI 打开 Wi‑Fi:用 settings put …、service call wifi …、cmd wifi enable(≥8.1)等写操作;提前确保 ROM 没把 Wi‑Fi 功能裁掉,或已在“工厂设置”里启用。祝你折腾顺利!
dumpsys wifi 里出现的那一行通常长这样:
1
Wi‑Fi is disabled
它只说明 系统目前把 Wi‑Fi 开关拨到了“关”,并不直接告诉你“有没有 Wi‑Fi 硬件”。要分清楚是“硬件存在但关掉”还是“根本没有硬件/驱动”,可以再看 4 处信号:
| 检查点 | 如何查看 | 典型输出 | 结论 |
| ——————- | ————————————– | ———————– | —————————————————- | ———————————————- | — |
| 1. Service 列表 | adb shell service list | grep -i wifi | wifi: [android.net.wifi.IWifiManager] | 看到这一行 → Framework 里 有 Wi‑Fi 服务 |
| 2. 驱动状态 | adb shell getprop wlan.driver.status | ok / ready / unloaded | 有值 → 内核认识 Wi‑Fi 芯片;空值 → 多半缺驱动/被裁掉 |
| 3. 网络接口 | adb shell ifconfig -a | grep wlan | wlan0 ... | 能看到 wlan0 → 芯片已初始化;完全没有 → 未加载 |
| 4. 内核模块 | adb shell lsmod \| grep -E "8188 | rt5370 | wlan" | | |
结果判读
情况
解释
解决办法
Service 有,wlan0 有,driver=ok,但 dumpsys 显示 disabled
硬件存在,只是软件开关关掉
用 UI 开关或adb shell settings put global wifi_on 1再重启 Wi‑Fi
Service 有,但 wlan0/驱动都没有
Framework 在,但 内核缺驱动/Factory 里关闭了 Wi‑Fi
先在工厂设置里勾选 WIFI 并保存;如仍无驱动 → 刷带 wifi_bt 的 ROM 或插 USB 网卡
Service 没有(grep 不到 wifi)
ROM/MCU 层完全裁掉了 Wi‑Fi
只能刷完整固件或硬改/外接网卡
Service 有,driver=unloaded
驱动文件在,但未成功加载
Factory 里未启用 / 模块版本不匹配 → 勾选保存或换匹配的系统+MCU
快速打开 Wi‑Fi(无 svc 的情况下)
1
2
3
4
adb shell settings put global wifi_on 1
adb shell am broadcast -a android.intent.action.WIFI_STATE_CHANGED --ez state true
# 然后再查看
adb shell dumpsys wifi | grep "Wi‑Fi is"
如果改完仍显示 disabled,就说明硬件/驱动层面还没点亮,需要回“工厂设置”勾选 Wi‑Fi 或换带驱动的固件。
一句话:dumpsys wifi 看到 disabled 只是“当前关着”,并不代表“没有 Wi‑Fi”。再看服务列表、驱动状态、wlan0 接口等就能分辨:
都在 → 只是没开;
驱动/接口缺失 → 硬件被关闭或驱动裁掉,需要在工厂设置启用或刷新固件。
在 Android 6.0(API 23)+ Linux 3.x 内核的 MT3561 车机上
下面所有命令都在 PC 端终端 输入(前面带 adb),车机端只需提前打开「USB 调试」。
1 功能概览:Android 6.0 能/不能用什么
功能
Android 6.0 支持情况
备注
cmd wifi …
❌ 没有(8.1 才加入)
svc wifi enable/disable
✅ 系统自带 toybox svc但你的 ROM 把 svc 删掉,才会显示 not found
settings put global wifi_on
✅ 无 root 也能写
service call wifi …
✅ 需要 root/SU
wpa_cli / iwlist / iw
取决于 ROM 是否编译进 /system/xbin
2 无 svc 情况下打开 Wi‑Fi(不需要 root)
1
2
3
4
5
6
# 打开 Wi‑Fi
adb shell settings put global wifi_on 1
adb shell settings put system wifi_on 1 # 某些 ROM 还要写 system 表
# 立即广播一次状态,让 Framework 赶紧刷新
adb shell am broadcast -a android.intent.action.WIFI_STATE_CHANGED --ez state true
校验开关是否生效
1
2
adb shell settings get global wifi_on # 应返回 1
adb shell dumpsys wifi | grep "Wi‑Fi is" # 应变成 enabled
如果还是 _disabled_,说明 Factory Setting 里仍把 Wi‑Fi 禁掉 或 驱动没加载——请回工厂菜单勾选 WIFI 或插免驱 USB 网卡后再试。
3 有 root 时的“硬开”——直接调 Binder
1
2
3
4
5
# 查询 wifi 服务是否存在
adb shell service list | grep -i wifi
# 调用 setWifiEnabled(true)
adb shell su -c 'service call wifi 13 i32 1' # 13 是 Android 6.* 通常的事务码
返回 result=0x1 表示成功;0x0 表示失败(多半被 Factory 关闭或驱动缺失)。
4 在 Android 6.0 上扫描 Wi‑Fi 热点
方法
是否 root
命令
settings + dumplog(最稳)
❌
```bash\nadb shell dumpsys wifi
wpa_cli(ROM 带 wpa_supplicant)
⭕
bash\nadb shell su -c 'wpa_cli scan'\nadb shell su -c 'wpa_cli scan_results'\n
iwlist(busybox)
⭕
```bash\nadb shell su -c ‘busybox iwlist wlan0 scanning
5 驱动检查——确认 8188EU/RT5370 是否加载
1
2
3
adb shell lsmod | grep -E "8188|rt5370|wlan"
adb shell getprop wlan.driver.status
adb shell ifconfig -a | grep wlan
结果
说明
后续
能看到 8188eu / rt5370sta 等模块
驱动已插
只需把 Wi‑Fi 开关打开即可
没有任何模块 / getprop 为空
内核缺驱动 or Factory 关闭
1) Factory 勾选 WIFI 保存 2) 刷带 wifi_bt 的 ROM3) 外接免驱 USB 网卡
6 Factory Setting 触摸不到 Wi‑Fi 条目的两大原因
MCU/System 不匹配 → 刷同型号、带 wifi_bt 标记的 全量包
硬件本身无 Wi‑Fi 芯片 → 外接 RTL8188EU / RT5370 USB 网卡(Android 6 内核 3.x 自带驱动)
一句话总结
在 Android 6.0 + Linux 3.x 的 evb3561sv 车机里:
svc 被删时,用 settings put global wifi_on 1 + 状态广播 就能无界面开 Wi‑Fi;
若有 root,也可 service call wifi 13 i32 1 直接写 Binder;
dumpsys wifi 看到 disabled 只是开关关着,不代表硬件不存在——再看 service list、lsmod、wlan0 就能判断;
真缺驱动或 Factory 关闭时,要么在隐藏菜单勾选 WIFI,要么刷带 Wi‑Fi 驱动的完整固件,或者插 RTL8188EU/RT5370 免驱 USB 网卡。祝你早点连上网!
shell: svc wifi enable 这条命令只是 Android 自带的小工具封装;
你那台 evb3561sv 的 ROM 把 toybox/toolbox 精简了,直接把 svc 可执行文件删掉,所以才提示 _not found_。
没关系,核心功能还在——我们可以绕过 svc,改用 系统设置 / Binder 服务 / cmd 三种办法来“硬切” Wi‑Fi。先按难度从低到高排一下,你照顺序尝试即可。
0 先确认两件事
检查
命令 (PC 端执行)
说明
车机是否连上 ADB
adb devices
看到 device 才算连通;若是 unauthorized,去车机点“始终允许”。
ROM 里有没有 cmd/settings/service
adb shell which cmd; which settings; which service
至少会有 settings 和 **service**;有哪个就用哪个。
1 用 settings 写系统值(多数无 root 机器可用)
1
2
3
4
5
6
7
8
9
10
# 开启 Wi‑Fi
adb shell settings put global wifi_on 1
adb shell settings put system wifi_on 1
# 立即让系统刷新一次广播(有的 ROM 必须)
adb shell am broadcast -a android.intent.action.WIFI_STATE_CHANGED --ez state true
# 可选:重启 Wi‑Fi 子系统(有 sh 脚本时)
adb shell stop wifi
adb shell start wifi
验证:
adb shell settings get global wifi_on → 返回 1;
adb shell dumpsys wifi | grep "Wi-Fi is" → 出现 enabled。
如果重启车机后又变回 0,说明 ROM 在启动脚本里把 Wi‑Fi 强行关掉——请继续看下一招。
2 用 service call wifi 直接打 Binder(需 root,但最稳)
先确认系统里有 wifi 服务:
1
2
adb shell service list | grep -i wifi
# 正常会看到 "wifi: [android.net.wifi.IWifiManager]" 之类
调用 setWifiEnabled(true)(在 Android 5/6 上通常是 事务码 13;如果无效换 6、8 试):
1
2
adb shell su -c 'service call wifi 13 i32 1' # 开
adb shell su -c 'service call wifi 13 i32 0' # 关
若提示 su: not found 就表示没 root;跳过这个方案或先刷 Magisk。
3 用 cmd wifi(Android 8.1‑O 以后才有)
1
2
adb shell cmd wifi enable
adb shell cmd wifi disable
在 MT3561 Android 8.1(Oreo)第三方 ROM 上测试可行;如果返回 Unknown command 就说明版本还在 7.1 以下,没有 cmd wifi 子命令。
4 如果上述全部无效 → 八成是 ROM 层面禁用 / 缺硬件
再回 Factory Setting
密码 3368 / 8888 → 勾选 WIFI → Save → 重启。
没有该项 = ROM 完全裁掉了 Wi‑Fi。
查驱动
1
2
adb shell lsmod | grep -E '8188|rt5370|wlan'
adb shell dmesg | grep -i wlan
空白 = 说明内核没加载任何 Wi‑Fi 模块。
解决思路
路线
说明
刷带 wifi_bt 字样的 完整 ROM + MCU
与主板编码 evb3561sv_w_65_m0 对应的版本,一键恢复 Wi‑Fi。
外接 RTL8188EU / RT5370 USB 网卡
车机对这两款芯片免驱,插上后重启即可在「网络与互联网」里出现 Wi‑Fi。
临时上网
手机 USB 共享网络 (Tethering) → 车机当“有线客户端”,不依赖 Wi‑Fi 射频。
常见报错对照
报错 / 现象
判断
处理
svc: not found
ROM 精简掉 toybox → 用 settings / service 方案
见步骤 1/2
settings: not found
工具也被删
装一个 busybox 或 toybox APK,或直接跳到 service call wifi
service call wifi … result=0x0 且 Wi‑Fi 仍 off
Binder 调用了,但返回 false
多半 “Factory” 里没勾选 Wi‑Fi 或硬件缺失
service list 根本没 wifi
内核 / Framework 把整个 Wi‑Fi Manager 移除了
只能刷新 ROM 或外接 USB 网卡
一句话总结
你的 ROM 砍掉了 svc 可执行文件,所以 svc wifi enable 无法用;先尝试 **settings put global wifi_on 1**,再不行就(有 root 时)用 **service call wifi 13 i32 1**。若系统连 wifi 服务都不存在,就说明 Wi‑Fi 在 Factory Setting 被禁用或 ROM/硬件干脆没装,最终只能刷含驱动的完整固件或外接免驱 USB 网卡。祝折腾顺利!
下面给你一份最通用、从零到一的操作清单,帮你重新进入 evb3561sv 车机的「工厂设置(Factory Setting)」隐藏菜单,并把 Wi‑Fi 模块启/停用。整套流程 3‑5 分钟,风险低,只要别在保存‑重启时断电即可。
步骤
详细做法
补充说明
1 回到主界面
点实体 🏠 键或手势返回,退出你现在的「运行命令」App,回到桌面。
2 打开系统设置
桌面找灰色齿轮 → 设置(Settings/设置中心/Car Setting)。如果桌面上没有,可在所有 App 列表里找“设置”或“车机设置”。
3 定位“工厂设置”入口
不同 ROM 位置略有差异,常见三种:① 系统 → 关于设备 → 工厂设置② Car Setting → 版本信息 页面右上角小齿轮/🔧③ 直接在主设置页底部看到 Factory/“工厂” 按钮
还有少数机型需要在顶部状态栏下拉,长按 ⚙️ 5 秒弹出“工厂模式”,可先试前两种常见路径再换招。
4 输入密码
出现数字键盘后依次试:• 8888(XY‑Auto 机型)• 3368(FYT 机型,多数 evb3561sv 属此族)• 3711、0000、123456 等备选
正确密码会立刻进入隐藏菜单;错了则原样返回,继续换密码即可。(XDA Forums, FCC Report)
5 勾选/取消 Wi‑Fi
进入后通常会看到 General / Feature / Hardware 等标签:• 找到 WIFI / WLAN / WIFI&BT 这一项• 启用 → 打勾;停用 → 取消勾选• 若有 BT 选项,可一并视需要勾选
字段名可能稍有区别,但一定带 “WIFI” 字样;看不到这一行多半是刷错 MCU 或 ROM 被阉割。
6 保存并重启
点页面底部 Save / Apply / OK;系统提示 Reboot now? 选 Yes,或手动重启一次电源。
期间千万别断 12 V 电源(ACC + B+)。
7 验证生效
重启后进 设置 → 网络与互联网:• 若出现 Wi‑Fi 开关并能扫描热点,说明启用成功;• 若仍无开关,则回隐藏菜单检查是否勾选保存成功,或确认硬件/驱动。
想进一步排错:adb shell getprop wlan.driver.status 应返回 ok/ready;空值说明驱动仍未加载。
如果根本找不到“工厂设置”按钮怎么办?
用 Activity Launcher 类工具
在 APK 市场搜索并安装 “Activity Launcher” 或 “QuickShortcutMaker”;
打开后搜索 factory / settings.FactorySetting / com.twd.factorysetting,点进去也能弹出隐藏菜单。
ADB 直接拉起
1
2
adb shell am start -n com.twd.settings/.FactorySetting # FYT/TS10 系常见
adb shell am start -n com.android.settings/.DeviceInfoFactory # 某些 XY‑Auto
若提示 Error type 3 仍找不到,说明 ROM 确实删掉了对应 Activity。
常见疑问速答
疑问
解答
密码都试完仍进不去?
可能设备品牌归属不同,联系卖家确认密码;或在 Activity Launcher 搜索 factory 直接拉 Activity。
选项勾了保存却自动跳回未勾?
MCU 与系统包不匹配 → 刷与主板版本一致、带 wifi_bt 标识的完整 ROM + MCU。
Wi‑Fi 列表能扫,但想让车机开热点?
默认无线芯片可能只支持 STA;需外接支持 AP 模式的 USB Wi‑Fi (RTL8188EU/RT5370) 或刷支持 hostapd 的内核。
关键提醒
操作完一定重启,否则配置不会写入 NVRAM。
切记保持 稳定供电,避免在行车或电池电压忽高忽低时操作。
建议首次进入隐藏菜单先点 Backup 或 Export Config 备份一份原始配置,方便误操作后恢复。
一句话:回桌面 → 打开「设置」→ 找到「工厂设置」→ 输入 8888/3368 等密码 → 在隐藏菜单勾选 WIFI → 保存并重启;若连入口都找不到,就用 Activity Launcher 或 ADB 命令直拉工厂设置界面,再按同样步骤操作。祝你成功点亮 Wi‑Fi!
---
## Proxmox Virtual Environment (PVE) 虚拟机安装不完全指南
- URL: https://blog.no-claw.com/posts/13af016a/
- 日期: 2025-05-10
- 分类: 电子产品 / 电脑 / NAS
- 标签: NAS
本文将详细介绍如何在 Proxmox Virtual Environment (PVE) 中安装虚拟机。我们将从系统安装开始,逐步引导您完成虚拟机的创建和配置。
准备工作
我选择使用 Ventoy 进行引导,这样可以避免反复使用 Etcher 等工具进行写盘操作。
系统安装
启动安装程序
启动后,您将看到如下界面:
用户协议
阅读并接受用户协议:
选择安装目录
选择安装 Proxmox VE 的磁盘:
设置密码
设置 root 用户的密码,邮箱地址可以随意填写:
配置网络
设置系统的 IP 地址:
开始安装
确认所有设置后,开始安装:
安装完成
安装完成后,系统将提示您访问 Web 管理界面,默认端口为 8006:
重启系统
拔掉引导 U 盘并重启系统:
登录与配置
登录系统
使用前面设置的密码登录系统:
Web 管理界面
登录后,您将看到 Proxmox VE 的 Web 管理界面:
测试网络连通性
确保网络连接正常:
创建虚拟机
上传 ISO 文件
首先,上传操作系统的 ISO 文件:
创建虚拟机
设置虚拟机编号
为虚拟机设置一个编号:
选择镜像
选择之前上传的 ISO 文件作为安装镜像:
配置硬件
主板信息
保持默认设置即可:
磁盘设置
建议选择 VirtIO 以提高性能:
CPU 配置
注意不要超过宿主机的 CPU 核心数,否则虚拟机将无法启动:
内存分配
根据需求分配内存:
网络设置
同样建议选择 VirtIO:
确认信息
确认所有设置无误后,点击完成:
启动虚拟机
启动虚拟机并开始安装操作系统:
操作系统安装
安装过程
按照提示进行操作系统安装:
加载驱动
在安装过程中,加载 VirtIO 驱动:
分区与安装
对磁盘进行分区并开始安装:
复制安装文件
安装程序将复制文件并进行系统安装:
设置时间
设置系统时区:
网络配置
安装完成后,系统可能暂时无法联网,稍后我们将安装网络驱动:
设置用户名和密码
为操作系统设置用户名和密码:
安装 VirtIO 驱动
安装 VirtIO 网络驱动以确保网络功能正常:
Linux 系统安装
对于 Linux 系统,安装过程类似,但通常 Linux 系统已经自带 VirtIO 驱动,无需手动安装:
通过以上步骤,您应该已经成功在 Proxmox VE 中安装并配置了虚拟机。希望本指南对您有所帮助!
---
## 懒猫微服实战入门(二):打造多人协作环境,权限控制+数据隔离一步到位
- URL: https://blog.no-claw.com/posts/4d17c8d/
- 日期: 2025-05-09
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服, MFA
最近在懒猫微服上架了一些应用,正好分享给家里人用。这个实现起来很简单,在懒猫微服上开一个新的用户,然后控制这个用户是否可以安装软件,可以使用什么软件,这样不同用户之间的数据就隔离开了,比如每个人的懒猫网盘和懒猫清单是独立的,互相不会产生干扰。
下面就以实际操作为例,教大家新建用户,并且了解这些自带的安全机制。
新建用户
设置 - 用户管理,这里可以看到现存的账户,第一次激活的时候会提示注册一个管理员账户,后面可以右上角点击邀请成员,然后会得到弹出一个二维码,新的客户端需要下载懒猫客户端,然后客户端扫码添加输入信息即可。
扫码后可以为新用户设置用户名和密码。客户端下载地址:
👉 https://lazycat.cloud/download
注册完成后,新成员就可以使用自己的账号登录懒猫微服啦。默认是“非管理员权限”,更安全。
用户权限控制
新建之后,我对这个新用户的画像是用户而不是管理者,所以只需要登录之后看到应用白名单就可以了。
点击新建用户的头像,可以设置用户可以看见应用的白名单。
比如,我只授权了家庭成员使用懒猫网盘、懒猫清单等基础工具。于是使用手机端登录新的账户,可以看到在我的应用中只有刚刚选中那些,这对于日常使用来说刚刚好,这个页面相对于安装了几十个 app 的管理页面来说,实在是清爽。
登录的二次验证
在此之前我们先来介绍多因子验证(MFA),通俗来讲我们在使用用户名密码登录的时候有时候还要接收一个验证码,有时候是发到邮箱的,有的是手机短信,还有个需要安装特定的 APP 来查看,比如 Authy,Google Authenticator。
而懒猫微服在 APP 中内置了 MFA 接收验证码的功能,新设备登录的时候会有如下提示:
在一台新的设备登录懒猫账户的时候,已经登录这个账户的设备就会弹出这个提示,这个时候我一般是提前打开懒猫微服 APP。
当然如果没有提前打开或者登录呢,也可以在「微服管理 - 安全码」来查看,比如这样,这个方式很 Apple 但是用着比 Apple 的提示舒服多了。
那么什么时候才会触发这个 MFA 呢?经过不完全测试,主要还是在这里设备管理这里有一个云端白名单,在这里的设备可以就可以免去 MFA 的验证,如果删除某个设备之后,这个设备会马上注销登录,并且在此登录的时候还需要 MFA 验证。这个操作,极大了降低了被黑客攻击的可能性。
当然如果你觉得 MFA 比较麻烦,那么也可以使用手机号码的方式进行登录,绑定手机号,然后用收验证码的方式进行登录,比如这样:
1
【懒猫微服】验证码:0000,5分钟内有效!请勿转发或泄漏。
写在最后
以前用过很多 NAS,一直苦于怕黑客攻击而没有监管 NAS 放在互联网,而一套完整的登录机制也要花费很大的精力去维护,拓展。期间也选择过蒲公英这样的异地组网设备,虽然可以达到目的,但是过程不尽如人意,对于很多国产生态来说,售后一直是缺失的很重要的一环。而懒猫微服恰好弥补了这样的短板,让懂技术的人从繁杂的维护设备中解放出来,像使用公有云一样的使用 NAS。
---
## 轻松管理 ES 集群:我把 Infinilabs Console 上架了懒猫微服,默认支持外网访问、
- URL: https://blog.no-claw.com/posts/75c015c8/
- 日期: 2025-05-08
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
之前我的 infinilabs Console 一直跑在群晖里,由于和 Coco-AI 的默认端口冲突,导致经常忘记端口信息,群晖里运行着 Easysearch,Elasticsearch、OpenSearch 三个大集群,也想慢慢迁移到其他性能高的机器上去,正好最近购买了懒猫微服,能够让我做应用的迁移,顺便还得能上架一些应用。
Infinilabs.console 是什么?
如果你用过 Elasticsearch,那就一定知道 Kibana。Infinilabs Console,就是极限科技团队开发的国产可视化控制台,是一个面向 Easysearch、Elasticsearch 和 OpenSearch 的运维、监控、数据管理平台,可以看作是国产版的 Kibana 替代品。
最初接触这个款产品的时候让我眼前一亮,它能够借助 Easysearch 或者 Elasticsearch 的 REST API 来连接集群,同时也高效地管理和监控 Elasticsearch、OpenSearch 以及 INFINI Easysearch 等搜索引擎集群,提供统一的运维、监控、安全和数据管理能力。这一点其实是 Kibana 比不了的,尽管是老牌软件,但是初学 ES 的时候 Kibana 连接 ES 要查 log 设置一些 key,这个整个部署过程就花了一个小上午的时间。而且跨版本,跨引擎来支持的能力也是其他可视化工具无法比拟的。简单来说,真的很符合国人的使用习惯。
首先我们可以在连接的时候不同的引擎(Easysearch、Elasticsearch、OpenSearch ),以及你集群的位置(线下还是在各种云上),同时支持 HTTP 和 HTTPS 的连接。
连接之后,可以看到已经正确识别出来了的 Easysearch、Elasticsearch 和 OpenSearch,并且抓取了相应的数据监控,比如基本的集群状态,节点数量,索引,分片以及文档的数量,还有磁盘和 JVM 的占用。
执行 DSL 的时候可以开启多个 TAB 页这个是我最喜欢的功能,尤其在做集群迁移的时候再也不用找不同的系统去登录了,这里手动@aws 的 OpenSearch。除此之外,做快照传到 S3 的时候也不用担心 access_key 读不到的问题了。曾经我是托管 OpenSearch 的用户,托管节点有诸多问题,无法登录,由于服务本身的问题导致业务滞后(升级卡住,看门狗不定时杀进程),做快照必须借助 Postman 来传递 IAM 凭证。但,换了 Infinilabs console 和 Easysearch 之后,整个世界都清净了。
GitHub 项目地址如下:https://github.com/infinilabs/console
为什么选择了懒猫商店?
懒猫微服解决了我日常使用 NAS 的几个痛点:
装了一堆服务(Redis、MinIO、MeiliSearch、Adminer、Swagger UI……),入口太分散;
每次看容器状态都要 docker ps 一把梭;
Homepage 要手动配置,配置文件写起来太繁琐;
部署成功后会给到一个域名,然后通过域名访问可以自动解析内外网的 IP 地址,同时也自带了路由守卫功能来重定向到懒猫的 SSO,
而在传统 NAS 部署 Authentik 然后再去应用端做 SSO 的适配应该是 NAS 玩家的终极梦想,而上架商店之后自动集成了这样的认证系统(也是单点登录)。然后,在外边的时候也可以监控和操作自己的 ES 集群啦~(随地大小班的理由又多了一条)
因为上架的应用是 HTTP 的,懒猫微服还能自动做了一个 TLS 传输,用的他们自己域名,然后通过 https 访问 Infinilabs Console。
除此之外还自带了 dozzle,可以很方便查看安装应用的上架信息,毕竟对于开发者来说,装机玩 NAS 是兴趣,但是搭建好之后的维护问题也同样劳心费力,真的一点都不想浪费时间和精力,那么杂活就交给平台来管理吧。
进入懒猫微服的【应用商店】,搜索:infinilabs.console一键安装并启动,打开浏览器,开始使用 Infinilabs Console 吧~
地址如下:
https://lazycat.cloud/appstore/#/shop/detail/xu.infinilabs.console
相关链接
infinilabs.console 介绍:https://infinilabs.cn/products/console/
infinilabs Github 介绍:https://infinilabs.cn/products/console/
懒猫微服上架地址:https://lazycat.cloud/
懒猫微服官网:https://lazycat.cloud/
---
## 懒猫微服实战入门(一): 从开启SSH到免密登录,一步步教你搞定远程管理
- URL: https://blog.no-claw.com/posts/56f9b737/
- 日期: 2025-05-07
- 分类: 懒猫微服 / 入门
- 标签: 懒猫微服, SSH
对于很多资深的 NAS 玩家来说,拿到一台机器首先要配置远程登录和环境依赖。懒猫上其实可以实现云计算讲的 Iass - Pass -Sass 这三个层级,不过对于资深玩家而言,肯定是要从 Infra 这个级别入手的。
官方文档如下:
https://developer.lazycat.cloud/ssh.html
安装懒猫开发者工具,然后再右上角能够看到 sshd 服务的状态。
然后点击开启,之后我们才可以使用 ssh 登录,在写这篇文章测试的时候,我关闭了这个按钮,再去 ssh 直接就报错了。
默认是 root 身份登录,密码在开发者工具里启动的时候设置:
1
ssh root@.heiyu.space
如果觉得密码麻烦,也可以导入密钥,更加安全:
1
ssh-copy-id -i ~/.ssh/id_ed25519 root@xxxxx.heiyu.space
输出如下:
1
2
3
4
5
6
7
8
9
10
11
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/Users/xu/.ssh/id_ed25519.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
root@micro.heiyu.space's password:
Number of key(s) added: 1
Now try logging into the machine, with: "ssh -i /.ssh/id_ed25519 'root@xxxx.heiyu.space'"
and check to make sure that only the key(s) you wanted were added.
如果觉得 root 用户不安全的话,可以新建一个日常用户,然后加到 docker 组里面,也能正常使用 docker
1
2
3
4
5
6
sudo useradd -m -s /bin/bash user1
usermod -aG docker user1
usermod -aG sudo user1
如果遇到到 root 组会有无法使用 sudo 的问题,请独立安装,sudo 是单独的软件包,需要安装才有.并不是所有 Linux 都有 sudo
1
apt update && apt install sudo
注意:要开着懒猫微服 APP ,否则无法使用 heiyu.space 提供的穿透服务。
---
## 快速检索懒猫商店1000+应用,微服秒变Mac原生APP
- URL: https://blog.no-claw.com/posts/ce3905e3/
- 日期: 2025-05-06
- 分类: 懒猫微服 / 番外
- 标签: 懒猫微服
懒猫商店如今已有 1000+ 应用,日常使用中经常要在搜索栏反复查找,着实有些不便。有没有更简单的方法呢?答案是:可以直接把网页保存成 Mac 应用,像手机 App 一样快捷打开!
下面就手把手教大家几种实用的方法。
优雅方案——PWA
在 Mac 上,我们有更高级的玩法。
不少现代网站都支持 PWA(Progressive Web App),简单来说,就是让网页像 App 一样运行:
可以像应用一样安装在本地
点击图标就能直接启动,无需打开浏览器
界面简洁,没有多余的地址栏和标签页
下面是懒猫清单的安装效果:
支持 PWA 的网站,在地址栏右侧会自动弹出“安装应用”按钮。
只需点击它,就能轻松将网页保存为应用。
PWA 的优点:速度快、体验好、支持离线,真正做到了网页与 App 的无缝结合。
通过 PWA 添加之后,会在 Finder 里弹出 Chrome 应用,我这里添加了懒猫网盘,懒猫原生的 APP 基本都是带 PWA 的,所以这一点体验很好。
添加完桌面应用之后,浏览器会有“在应用中打开”的提示,点击就可以像 APP 一下打开,就是前面第二张懒猫清单的图片。
如何通过 Chrome 中安装懒猫 Web 应用
在 Chrome 浏览器中打开你要保存的网站(如懒猫微服务)。
点击右上角“更多”按钮,依次选择**投放、保存和分享 → 将网页安装为应用…**。
有些网站也会直接在地址栏右侧显示“安装”图标,点一下即可快速安装。
安装时你可以自定义应用名称,这里以 OnlyOffice 为例。
这样做还可以解决 Mac 没有 Office 订阅的痛点,直接通过网页版弥补。
完成后,应用会存放在:
1
/Users/你的用户名/Applications/Chrome Apps.localized/
它们会以.app格式存在,完全就像普通 Mac 应用一样。
1
2
3
❰~/Applications/Chrome Apps.localized❱✔≻ ls
Icon? 懒猫清单.app/
ONLYOFFICE Docs.app/ 懒猫网盘.app/
如何通过 Safari 中把懒猫应用添加为 APP
对于不支持 PWA 的网站,Safari 也提供了一个类似的解决方案。
在 Safari 中打开要保存的网页。
选择“文件 → 添加到程序坞”,或者点击“共享”按钮 → 添加到程序坞。
输入自定义的应用名称,点击“添加”。这个应用会自动放在应用程序里面。
应用将会被保存到“应用程序”文件夹中,支持从程序坞、启动台或 Spotlight 快速启动。
直接拖拽到 Dock,一键启动
无论是通过 Chrome 还是 Safari 安装的网页 App,安装完成后都可以像普通应用一样拖到 Dock。
只需保持懒猫微服务后台连接,点击 Dock 图标,就能立即打开应用,体验和原生 App 无异!
进阶玩法:自定义网页启动器
当然,你也可以用 Python 快速实现一个简单的网页启动器:
1
2
3
import webbrowser
webbrowser.open("https://www.apple.com") # 打开网页
支持新窗口、新标签等操作,适合简单自定义。
结语
通过以上方法,我们就可以把常用的懒猫 APP 变成 Mac 的桌面应用,随时一键直达,告别繁琐的搜索过程,体验飞跃式提升!
---
## MySQL数据实时接入Easysearch,零代码迁移全流程
- URL: https://blog.no-claw.com/posts/7a296932/
- 日期: 2025-05-04
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
随着业务对数据搜索与分析能力的要求不断提高,越来越多的团队开始将关系型数据库中的数据迁移至搜索引擎中。
本篇文章将分享如何通过AWS DMS,实现 MySQL 数据无缝迁移到EasySearch,并打通实时同步链路的全过程。
注意:AWS DMS 原生仅支持托管的 OpenSearch 和 Elasticsearch,不直接支持 EasySearch。本文将介绍如何通过一些配置技巧,优雅地解决这个问题。
一、准备 MySQL 源数据库
1. 创建数据库与数据表
首先,我们需要准备好待迁移的 MySQL 数据库。我这里使用的是DBeaver工具,当然你也可以选择更专业的 MySQL Workbench 或 DataGrip。
新建数据库时,选择utf8mb4编码,库名命名为source(后续 DMS 迁移任务中会用到)。
接下来创建数据表并定义字段。图形化工具可以避免手写 DDL,对非 DBA 用户非常友好。
2. 插入测试数据
为了验证迁移效果,我们先写几条假数据。
确认数据已成功写入并提交。
二、配置 AWS DMS 迁移
1. 创建源端点(MySQL)
在 AWS DMS 中,首先需要定义源端点。MySQL 作为数据源,EasySearch 作为目标端。
2. 创建目标端点(EasySearch)
我的 EasySearch 部署在公网的 Linux 服务器上。配置目标端点时,有两个重点:
关闭认证与 TLS,将传输协议改为 HTTP
伪装为开源 Elasticsearch,绕过 DMS 的原生认证要求
只需要调整这两个关键参数,即可完成兼容。
3. 创建复制实例
迁移任务需要一个复制实例,即 DMS 后台自动启动的迁移代理服务器。
4. 测试连接
实例启动后,记得测试源端(MySQL)和目标端(EasySearch)的连通性,确保网络正常。
三、创建迁移任务并启动
在控制台新建迁移任务
1. 预检查:开启 MySQL Binlog
为了支持 CDC(持续复制),需要提前在 MySQL 开启 binlog,并调整格式为ROW。
2. 创建迁移任务
在 DMS 中创建任务,选择持续复制模式,源库填写source,EasySearch 会自动将数据表转为索引。
注意:目标端为 EasySearch 时需要关闭数据验证,否则迁移任务会因兼容性问题失败。
四、验证迁移效果
1. 启动任务并查看索引
EasySearch 初始化状态下只有默认索引。
启动任务后,DMS 自动创建了一个新索引newtable,映射 MySQL 的数据表。
打开索引,可以看到 MySQL 数据已转换为 EasySearch 的文档格式。
2. 模拟实时数据同步
因为是 CDC 持续复制模式,我继续向 MySQL 插入新数据,模拟上游系统的实时写入。
EasySearch 这边几乎实时就收到了新数据,验证了迁移链路的连贯性。
五、总结
本次实战中,借助 AWS DMS,我们实现了从 MySQL 到 EasySearch 的实时数据同步,具备以下优势:
无需改造业务系统,兼容现有 MySQL 数据结构
支持 CDC,保证数据的实时同步与一致性
EasySearch 原生接入,数据即迁即用
虽然 AWS DMS 默认并不支持 EasySearch,但通过合理配置与兼容策略,我们依然实现了两者的高效打通。
如果你的业务需要将 MySQL 数据实时同步到 EasySearch,这套方案值得一试。
---
## 自带内网穿透,懒猫微服治好了我的NAS焦虑
- URL: https://blog.no-claw.com/posts/b1e8232f/
- 日期: 2025-05-03
- 分类: 懒猫微服 / 番外
- 标签: 懒猫微服, NAS
知道懒猫微服是一两年前,最初的印象是极客机甲风格,有颜值有性能有。近期入手也是出于朋友推荐,主要还是喜欢自带的内网穿透功能,虽然家里有公网 IP,但是不定时会被运营商封端口(不止常用端口),总是一阵一阵的,询问运营商也没有一个明确的结论,倒不如选一个商业的产品然后省去自己折腾的时间吧
虽然我一直很想 DIY 这样一款产品,开源的大多是 KVM-base 的方案,或者还有商业的 EXSI。毕竟个人精力有限,一直搁置到现在,然后随着事情越来越多,就购买了一台来玩玩,也脱胎换骨当甲方提需求。官网如下:https://lazycat.cloud/
机器是这样的,浓浓的机甲风格连,着运行了几天也一点都不烫。之前还担心炒豆子的问题,其实相对于白天的噪音几乎是没有的。
i5-1135G7,现在来看不算最新的,不过也比很多 NAS 强很多了,手动@群晖
32G 内存(只能一个盘位,所以加满了)
2.5 寸 2T 原装 HDD(预算有限,目前还在测试阶段,自带的盘是叠瓦盘,介意的话可以自己买盘替换)
neofetch 能看到是基于 Debian12 的。然后开发团队在上层构建自己的应用,只是 ssh 需要额外申请,不过一会就批了。
提供全平台的客户端,该有的都有了,这里开发适配应该花了不少时间吧。
MacOS 客户端的界面如下, 如果通过 web 访问就是这样,和传统的群晖有个主页不一样,这个更像是服务导向的,对于小白来说,只需要按照 Sass 的方式来使用,比如文件备份,时间机器,异地组网。而对于技术人员来讲,我个人觉得是更加吃力一些,要搞清楚每个服务是怎么启动的,怎么保证网络传输,怎么保证 HA,尽管懒猫团队已经实现了这些,但是出于职业习惯,还是希望抽丝剥茧,搞清楚从 Iass 到 Sass 的原理,然后学一学背后的哲学,以及在懒猫的商店上架自己应用,还有把应用接入懒猫的 SSO 系统。
关于服务嘛,是大多数玩家最喜欢的地方,有专门的 N 对 1 答疑,7*18 服务比 7 *24 听起来要靠谱很多,只能说专业,太专业了。团队是 deepin 二次创业出来的,都是技术流,所以懂用户,在群里可以做深度的技术答疑。在我之前的感受就是,玩 nas 的太多是爱好者,很多不懂 Linux 和开发,或者懂技术的都是理论派,对自己 host-server 或者 application 没有兴趣。以前的同事能因为这个结缘,由于彼此方向不同更多会集中在 Iass 和 network 的层面,而端对端的解决方案甚少。
但是懒猫让我看到了未来 nas 进化的方向,从硬件 - Iass -pass - Sass 做了全套的定制,也做了我一直想做而没有做完的事情,最早我的想发是在 Centos 上用 docker 跑很服务,然后用 NFS 做 share,然后用 KVM 做虚拟化层,然后用商业的方案做异地组网。尽管过过程十分坎坷,遇到了硬盘噪音,纯开源项目支持不到位,商业方案售后不专业等问题,最后就只在内网使用,走了很多弯路吧。
相信懒猫的这个价格,如果用 AWS 的话,最多半年就烧光 credit 了。有如此专业的团队来支持,治好了我的 NAS 焦虑。
文章来源:
---
## 使用 Nginx 获取并返回客户端的公网 IP 地址
- URL: https://blog.no-claw.com/posts/92ab0084/
- 日期: 2025-04-23
- 分类: 软件
- 标签: 网关, Nginx
在某些场景下,我们需要快速获取客户端的公网 IP 地址。虽然有许多在线服务(如 IP Address Lookup、IPv4/IPv6 检测工具或https://checkip.amazonaws.com/)可以提供此功能,但通过自建Nginx服务来实现这一需求,不仅灵活可控,还能更好地满足个性化需求。
下面是一个简单的 Nginx 配置示例,用于返回客户端的公网 IP 地址。
配置 Nginx 返回客户端 IP 地址
如果你希望 Nginx 直接返回客户端的 IP 地址,可以通过在location块中使用$remote_addr变量来实现。以下是一个完整的配置示例:
1
2
3
4
5
6
7
8
server {
listen 80;
location /get_ip {
default_type 'application/json';
return 200 '{"ip_addr": "$remote_addr"}';
}
}
配置说明:
listen 80;
监听 80 端口,处理 HTTP 请求。
location /get_ip { ... }
定义一个路径为/get_ip的请求处理块。当客户端访问/get_ip时,Nginx 会执行该块中的指令。
default_type 'application/json';
设置响应的默认 MIME 类型为application/json,确保客户端能够正确解析返回的 JSON 数据。
return 200 '{"ip_addr": "$remote_addr"}';
返回一个 HTTP 状态码为 200 的响应,内容为一个 JSON 对象,其中ip_addr字段的值为客户端的 IP 地址(通过$remote_addr变量获取)。
示例响应
当客户端访问/get_ip路径时,Nginx 会返回如下格式的 JSON 响应:
1
{ "ip_addr": "客户端IP地址" }
例如,如果客户端的 IP 地址是203.0.113.1,则响应为:
1
{ "ip_addr": "203.0.113.1" }
总结
通过以上配置,你可以快速搭建一个简单的 Nginx 服务,用于返回客户端的公网 IP 地址。这种方式不仅高效,还能根据需求进一步扩展功能,例如记录 IP 地址、限制访问频率等。如果你需要更复杂的功能,可以结合 Nginx 的其他模块和变量来实现。
---
## 从认证到透传:用 Nginx 为 Easysearch 构建一体化认证网关
- URL: https://blog.no-claw.com/posts/c50511c4/
- 日期: 2025-04-23
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
在构建本地或云端搜索引擎系统时,EasySearch 凭借其轻量、高性能、易部署等优势,逐渐成为众多开发者和技术爱好者的首选。但在实际部署过程中,如何借助 Nginx 为 EasySearch 提供高效、稳定且安全的访问入口,尤其是在身份认证方面,仍然是一个关键技术环节。
本教程将围绕 Basic Auth 认证机制展开,系统讲解如何通过 Nginx 实现安全防护、认证信息透传等常见配置场景,帮助你在多种实际部署环境中快速搭建可靠的访问控制机制。
无论你是在搭建家庭 NAS 服务,还是在企业环境中集成搜索引擎系统,本教程都能为你提供一套可落地、可复用的 Nginx 安全认证解决方案。
下面是我的 Nginx 配置文件示例。我们通过 Docker 启动 Nginx 容器,并将本地编写好的配置文件挂载到容器中,从而实现自定义的反向代理和认证逻辑:
1
2
3
4
5
docker run -d \
--name my-nginx \
-p 80:80 \
-v $(pwd)/default.conf:/etc/nginx/conf.d/default.conf \
nginx
default.conf配置如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
server {
listen 80;
server_name localhost;
# 根路径可选配置,如果你要一个欢迎页
location / {
return 200 'Nginx is running.\n';
add_header Content-Type text/plain;
}
# 反向代理 EasySearch
location /es/ {
proxy_pass https://backend:9200/;
# 修正请求头
proxy_http_version 1.1;
# proxy_pass_request_headers on; ÷
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 如果需要保活连接
proxy_set_header Connection "";
# 可选:允许跨域访问(用于前端 AJAX 调试)
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'Authorization,Content-Type';
# proxy_set_header Authorization "Basic YWRtaW46MTIzNDU2";
# 清理路径前缀 `/es/`
rewrite ^/es/(.*)$ /$1 break;
}
# 可选:静态资源支持
# location /static/ {
# root /usr/share/nginx/html;
# }
}
🌐 配置整体结构
1
2
3
server {
listen 80;
server_name localhost;
监听端口:监听本地 80 端口(HTTP 默认端口)
服务名称:用于匹配请求的 Host,这里是 localhost
🎉 欢迎页(根路径 /)
1
2
3
4
location / {
return 200 'Nginx is running.\n';
add_header Content-Type text/plain;
}
请求 / 会返回纯文本响应 "Nginx is running.",可用于验证 Nginx 是否启动正常。
add_header Content-Type text/plain:指定响应内容为纯文本。
🔁 /es/ 代理 EasySearch 后端服务
🚚 请求头处理
1
2
3
4
5
6
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_http_version 1.1:确保代理使用 HTTP/1.1,支持长连接。
Host:保留原始请求的主机名。
X-Real-IP / X-Forwarded-For:传递客户端真实 IP。
X-Forwarded-Proto:传递原始协议(http / https)。
Connection "":用于避免默认的 keep-alive 设置引起错误(推荐保留)。
🔐 可选的认证头(注释中)
1
# proxy_set_header Authorization "Basic YWRtaW46MTIzNDU2";
可选开启,用于将认证信息转发到后端。
上面的字符串是 admin:123456 的 base64 编码。可以根据需要开启。
🌍 CORS 设置(跨域)
1
2
3
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'Authorization,Content-Type';
允许任意源访问(前端页面可以跨域请求 /es/)。
支持的方法:GET、POST、OPTIONS。
允许传递的请求头:Authorization 和 Content-Type。
✅ 适用于 AJAX 调试、前后端分离等场景。
🔧 URL 重写
1
rewrite ^/es/(.*)$ /$1 break;
移除 /es/ 前缀,转发给后端。例如:
用户请求 /es/_cat/indices 实际转发到 /cat/indices。
break 表示在当前 location 中中止后续重写检查。
📦 可选静态资源(被注释掉)
1
2
3
# location /static/ {
# root /usr/share/nginx/html;
# }
若开启,可以直接通过 /static/xxx.js 访问 Nginx 本地 /usr/share/nginx/html/static/xxx.js 文件。
🔁 如果你想保留 /es/ 前缀,则删除 rewrite 行。
在启动服务后,当我们通过浏览器访问 Nginx 时,页面会弹出身份验证窗口。需要注意的是,这里的认证提示实际上来自后端的 EasySearch 服务,而非 Nginx 本身,说明请求中的认证信息未在 Nginx 层被处理或透传,因此由 EasySearch 发起了再次认证的要求。
在输入正确的用户名和密码后,我们可以看到 Nginx 成功代理请求,并返回了来自 EasySearch 的 API 响应,说明认证流程已顺利通过,后端服务正常可用。
如果希望由 Nginx 代为完成 EasySearch 的身份验证,也就是实现自动登录的效果,可以在配置文件中添加如下指令,将认证信息通过 HTTP 头部传递给后端:
1
proxy_set_header Authorization "Basic YWRtaW46YWRtaW4=";
其中,YWRtaW46YWRtaW4xMjM= 是使用 Base64 编码后的 用户名:密码 字符串(例如 admin:admin)。Nginx 在转发请求时会自动携带该 Header,从而实现对 EasySearch 的自动认证。
需要注意的是,Nginx 的配置文件修改后不会自动生效。为了确保配置被正确加载,需在每次更改配置文件后重启对应的容器服务。这是容器化部署中常见的操作流程,确保新配置被正确应用。
为了更直观地观察请求行为,我们使用了 Postman 进行测试。可以发现,即使在 Postman 中未显式添加任何认证信息,依然能够成功访问 EasySearch 集群。这说明前端未输入认证信息,但由于 Nginx 曾经缓存了认证状态,或配置了自动透传,导致后端依旧接收到了有效的认证头,从而允许了访问。
这种现象虽然在测试中提升了访问便利性,但在实际部署中可能带来安全风险,因此在生产环境中建议对认证流程进行严格控制,确保后端服务不会因为前端认证机制的疏漏而被绕过。
在一些教程中,常会提到一个名为 .htpasswd 的文件,它用于在 Nginx 层实现基本认证。当启用该机制后,Nginx 会对所有访问进行用户身份验证。
此时,是否将认证信息透传给后端服务,则由 proxy_pass_request_headers 参数决定。该参数默认值为 on,也就是说,当认证通过后,客户端发送的 Authorization 头部信息会被 Nginx 一并转发给后端服务。
为了验证这一行为,我编写了一个简单的 Flask 程序作为后端,用于观察请求中的 Header 内容。在真正将请求代理至 EasySearch 之前,我先让 Nginx 将请求反向代理到这个 Flask 应用,从而可以直观地查看是否存在 Authorization 头被透传的情况。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from flask import Flask, request,abort
import base64
app = Flask(__name__)
@app.route('/')
def hello_world():
print("📥 Headers received from Nginx:")
print("Host:", request.headers.get('Host'))
print("X-Real-IP:", request.headers.get('X-Real-IP'))
print("X-Forwarded-For:", request.headers.get('X-Forwarded-For'))
print("X-Forwarded-Proto:", request.headers.get('X-Forwarded-Proto'))
print(request.headers)
auth_header = request.headers.get('Authorization')
print("Authorization:", auth_header)
if not auth_header or not auth_header.startswith('Basic '):
abort(401, description="Missing or invalid Authorization header")
# 解码 base64
encoded = auth_header.split(' ')[1]
decoded = base64.b64decode(encoded).decode('utf-8') # e.g. admin:123456
username, password = decoded.split(':', 1)
print(username, password)
return 'Hello World!'
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000,debug=True)
这个是 flask 的打印的结果.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Host: secure-nginx.orb.local
X-Real-IP: 192.168.215.1
X-Forwarded-For: 192.168.215.1
X-Forwarded-Proto: http
Host: secure-nginx.orb.local
X-Real-Ip: 192.168.215.1
X-Forwarded-For: 192.168.215.1
X-Forwarded-Proto: http
Authorization: Basic YWRtaW46YWRtaW4=
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh-TW;q=0.9,zh;q=0.8
Cookie: perf_dv6Tr4n=1
Authorization: Basic YWRtaW46YWRtaW4=
admin admin
192.168.X.X - - [24/Apr/2025 15:55:59] "GET / HTTP/1.1" 200 -
为了解决双重认证的问题,我们启用了认证信息透传的配置(默认的 roxy_pass_request_headers on;)。启用该配置后,用户只需在访问 Nginx 时进行一次手动身份验证。Nginx 会将用户提供的凭证通过 HTTP Header 透传至后端的 EasySearch 服务,从而避免二次认证。当用户直接访问 EasySearch 时,依然需要单独输入凭证进行认证;但通过 Nginx 访问时,只需在前端认证一次即可完成整个请求流程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
curl -k https://easysearch:9200
{"error":{"root_cause":[{"type":"security_exception","reason":"Missing authentication information for REST request [/]","header":{"WWW-Authenticate":"Basic realm=\"Security\" charset=\"UTF-8\""}}],"type":"security_exception","reason":"Missing authentication information for REST request [/]","header":{"WWW-Authenticate":"Basic realm=\"Security\" charset=\"UTF-8\""}},"status":401}⏎
-------
curl -v -u "admin:admin" http://nginxhost/es/
* Trying 192.168.5.171:9201...
* Connected to 192.168.5.171 (192.168.5.171) port 9201
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
* CAfile: /etc/ssl/cert.pem
* CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Request CERT (13):
* (304) (IN), TLS handshake, Certificate (11):
* SSL certificate problem: self signed certificate
* Closing connection
curl: (60) SSL certificate problem: self signed certificate
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
* Host localhost:80 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:80...
* Connected to localhost (::1) port 80
* Server auth using Basic with user 'admin'
> GET /es/ HTTP/1.1
> Host: localhost
> Authorization: Basic YWRtaW46YWRtaW4=
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Server: nginx/1.27.4
< Date: Thu, 24 Apr 2025 07:45:10 GMT
< Content-Type: application/json; charset=UTF-8
< Content-Length: 552
< Connection: keep-alive
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: GET, POST, OPTIONS
< Access-Control-Allow-Headers: Authorization,Content-Type
<
{
"name" : "easysearch-node1",
"cluster_name" : "infinilabs",
"cluster_uuid" : "VcMD__DwSYSUqear8wp-XA",
"version" : {
"distribution" : "easysearch",
"number" : "1.11.1",
"distributor" : "INFINI Labs",
"build_hash" : "4d0be0343919fb1a605e3c8284326b7e069eb9bf",
"build_date" : "2025-03-14T09:33:12.182925Z",
"build_snapshot" : false,
"lucene_version" : "8.11.4",
"minimum_wire_lucene_version" : "7.7.0",
"minimum_lucene_index_compatibility_version" : "7.7.0"
},
"tagline" : "You Know, For Easy Search!"
}
* Connection #0 to host localhost left intact
本次将 Nginx 的访问认证密码修改为 admin123 后,发现在请求过程中出现了两次身份验证的提示。具体表现为:当用户输入错误的密码时,Nginx 会首先返回一次 401 Unauthorized。由于 Nginx 与 EasySearch 使用了不同的认证信息,Nginx 在将请求头(包括 Authorization 字段)转发至 EasySearch 时,EasySearch 检测到凭据不匹配,也会返回一次 401。由此导致了双重身份认证失败的现象,影响了正常访问流程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
curl -v -u "admin:admin" http://localhost/es/
* Trying 192.168.5.171:9201...
* Connected to 192.168.5.171 (192.168.5.171) port 9201
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
* CAfile: /etc/ssl/cert.pem
* CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Request CERT (13):
* (304) (IN), TLS handshake, Certificate (11):
* SSL certificate problem: self signed certificate
* Closing connection
curl: (60) SSL certificate problem: self signed certificate
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
* Host localhost:80 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:80...
* Connected to localhost (::1) port 80
* Server auth using Basic with user 'admin'
> GET /es/ HTTP/1.1
> Host: localhost
> Authorization: Basic YWRtaW46YWRtaW4=
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 401 Unauthorized
< Server: nginx/1.27.4
< Date: Thu, 24 Apr 2025 09:21:09 GMT
< Content-Type: text/html
< Content-Length: 179
< Connection: keep-alive
* Authentication problem. Ignoring this.
< WWW-Authenticate: Basic realm="Restricted Area"
<
401 Authorization Required
401 Authorization Required
nginx/1.27.4
* Connection #0 to host localhost left intact
❰xu❙~/OrbStack/docker/containers/secure-nginx/etc/nginx❱✔≻ curl -v https://192.168.5.171:9201/ (base) 17:21:09
curl -v -u "admin:admin123" http://localhost/es/
* Trying 192.168.5.171:9201...
* Connected to 192.168.5.171 (192.168.5.171) port 9201
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
* CAfile: /etc/ssl/cert.pem
* CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Request CERT (13):
* (304) (IN), TLS handshake, Certificate (11):
* SSL certificate problem: self signed certificate
* Closing connection
curl: (60) SSL certificate problem: self signed certificate
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
* Host localhost:80 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:80...
* Connected to localhost (::1) port 80
* Server auth using Basic with user 'admin'
> GET /es/ HTTP/1.1
> Host: localhost
> Authorization: Basic YWRtaW46YWRtaW4xMjM=
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 401 Unauthorized
< Server: nginx/1.27.4
< Date: Thu, 24 Apr 2025 09:21:16 GMT
< Content-Type: application/json; charset=UTF-8
< Content-Length: 381
< Connection: keep-alive
* Authentication problem. Ignoring this.
< WWW-Authenticate: Basic realm="Security" charset="UTF-8"
<
* Connection #0 to host localhost left intact
{"error":{"root_cause":[{"type":"security_exception","reason":"Missing authentication information for REST request [/]","header":{"WWW-Authenticate":"Basic realm=\"Security\" charset=\"UTF-8\""}}],"type":"security_exception","reason":"Missing authentication information for REST request [/]","header":{"WWW-Authenticate":"Basic realm=\"Security\" charset=\"UTF-8\""}},"status":401}⏎
场景编号
Nginx 是否开启认证
EasySearch 是否开启认证
实际认证次数
说明
①
否
否
0 次
完全开放,任何请求无需验证。
②
否
✅ 是
1 次
访问时直接弹出 EasySearch 的认证窗口,用户需输入凭证。
③
✅ 是
否
1 次
仅在 Nginx 层验证,验证通过后直接访问后端。
④
✅ 是
✅ 是
2 次(默认)
Nginx 和 EasySearch 各自认证,用户需连续输入两次密码。
⑤
✅ 是
✅ 是
1 次(透传,proxy_pass_request_headers on;)
Nginx 开启认证,并通过 proxy_set_header Authorization 透传给 EasySearch,用户仅需输入一次密码即可完成认证。
---
## 威联通 QNAP 系列 (一) 全闪 NAS TBS-h574TX QuTS hero 初探
- URL: https://blog.no-claw.com/posts/7acb32ac/
- 日期: 2025-04-23
- 分类: 电子产品 / 电脑 / NAS / QNAP
- 标签: 搜索引擎(ES)
字数 1371,阅读大约需 7 分钟
都说 2024 是全闪 NAS 的元年,各个厂商也纷纷出品的自家的 NAS,独占鳌头的还是 QNAP 的TBS-h574TX,5 盘位 NVME,支持 10G 网口以及雷电网桥,甚至还有 12 代 i5 CPU 这个配置很难不让人心动。
初始化系统安装
使用 Qfinder Pro 可以查找局域网内的 QNAP NAS, 免去手动查找 IP 的麻烦,软件支持全平台。
Qfinder 寻找结果如下,可以识别出 NAS 名称,IP 地址,MAC 地址,机器型号以及系统及其版本。
除此之外,我们也可以在路由器后台寻找 IP 地址。
开始初始化流程:
进入 web 页面,开始安装系统
授权条款如下:
选择固件版本,为了快速安装,一般选择当前版本,然后进入 OS 内部再进行升级
设置 nas 名称和用户名,密码,由于系统内置了 admin 用户,所以这里不能使用 admin
设置时区以及 NTP 服务器同步时间。
设置 IP 地址,可以选择 DHCP 或者静态地址,我一般为了方便选 DHCP,这些后期都可以系统内部进行修改。
如果设置静态地址需要的参数如下:IP 地址,子网掩码,网关,DNS 服务器,不过还是建议直接在路由器上设置静态 IP 方便管理。
这个机器支持了雷电 4 的接口,原生支持了雷电网桥,所以这里多出来一个检测雷电的步骤,拔插雷电的时候机器会滴滴的响几声。
固件更新设置,建议设置通知,手动更新。
这个初始化的步骤会清除硬盘上的所有数据。
接下来就几分钟的等待,主要是等待往硬盘上安装软件,可以看到进度条。
这个机器的配置比较高,全程用了五分钟左右,这里主要启动 SMB 和系统的一些进程。
设置完毕之后,我们就可以使用 NAS 了。
使用之前设置好的用户名和密码进行登录
初始化磁盘
第一步需要设置存储池,用我们安装的 NVME 硬盘组成一个 raid 存储池。
RAID 通过将多个硬盘组合在一起,形成一个更大的存储单元,以实现数据冗余备份或提升存储性能。它是一种存储虚拟化技术,可以让系统同时从多个硬盘中读取和写入数据,从而提高读写速度。
RAID 的常见级别如下:
• RAID 0(条带化):数据被分割成多个部分,分别存储在多个硬盘上,读写速度快,但没有冗余保护,任意一块硬盘损坏会导致数据丢失。(建议谨慎使用)
• RAID 1(镜像):数据在两个硬盘上存储两份,每次写操作都会写到两个硬盘上,数据安全性高,但磁盘利用率低。
• RAID 5(分布式奇偶校验):利用条带化和奇偶校验实现数据保护,至少需要三块硬盘,能够在提高存储性能的同时保证数据的冗余性,可以容忍一块盘故障。
• RAID 6(双奇偶校验):与 RAID 5 类似,但使用双重奇偶校验,可以容忍两块硬盘同时故障。
• RAID 10(镜像+条带化):结合了 RAID 1 和 RAID 0 的优点,先进行镜像操作,再进行条带化,提供高性能和高可靠性。
我只有两个 NVME,所以出于测试目的,组建了 Raid0。
设置存储池预留空间,快照预留空间以及警报阈值。
可以看到我的 4T 和 2T 的 NVME 组建的 RAID0 阵列, 设置完毕,除了保留空间外,最后之后 3.6T 可以用。
同样存储池也会清除磁盘上的所有数据。
存储池磁盘位一览:
’ fill=’%23FFFFFF’%3E%3Crect x=’249’ y=’126’ width=’1’ height=’1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
存储池目录结构如下:
’ fill=’%23FFFFFF’%3E%3Crect x=’249’ y=’126’ width=’1’ height=’1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
简单的监控
在用户管理处我们可以看到刚刚设置的用户,在这里也可以新建用户做一些额外的权限控制。
’ fill=’%23FFFFFF’%3E%3Crect x=’249’ y=’126’ width=’1’ height=’1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
同时 NAS 还自带了监控,可以看到 CPU,内存以及磁盘使用率,还有运行时间,风扇转速,访问记录等等。
’ fill=’%23FFFFFF’%3E%3Crect x=’249’ y=’126’ width=’1’ height=’1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
日志处理
QuLog Center 是一款集中日志管理应用程序,可将详细的系统事件、系统访问和在线用户状态记录到您的设备。收集的信息可用于有效地诊断和理解设备系统问题,例如与用户访问相关的记录
’ fill=’%23FFFFFF’%3E%3Crect x=’249’ y=’126’ width=’1’ height=’1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
QuLog 服务用于将日志传输到其他设备的 QuLog Center。您可以将其他设备的日志集中起来管理。
’ fill=’%23FFFFFF’%3E%3Crect x=’249’ y=’126’ width=’1’ height=’1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
日志也可以发送到 Syslog 服务器:
’ fill=’%23FFFFFF’%3E%3Crect x=’249’ y=’126’ width=’1’ height=’1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
公网访问
如果想在公网上访问这个 NAS,那么也可以在路由器上设置端口转发,使用自家的 IP 地址进行访问。
’ fill=’%23FFFFFF’%3E%3Crect x=’249’ y=’126’ width=’1’ height=’1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
写在最后
全闪主要是风扇的声音比较大,用手机贴在上面测试了下,在 50 分贝左右,拿开一段距离的话在 40 分贝左右。有条件还是放在柜子里吧。
’ fill=’%23FFFFFF’%3E%3Crect x=’249’ y=’126’ width=’1’ height=’1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
---
## Easysearch 服务昨天还好好的,为什么今天突然访问不了了?
- URL: https://blog.no-claw.com/posts/8d9611a5/
- 日期: 2025-04-22
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
在使用 Easyearch 搭建本地或云端搜索服务时,难免会遇到服务偶发性或持续性无法访问的问题。本文将从服务状态、端口监听、网络配置、安全组等五个维度,系统梳理排查思路,帮助大家快速定位并解决访问失败的原因。
🚢 一、Docker 部署场景下的排查方法
如果你是通过官方的 Docker Compose 部署 EasySearch,一般不会出现太大问题。但如果你像我一样在群晖或 NAS 上做过自定义配置,以下通用排查方法可以帮助你快速定位问题:
示例 Docker Compose 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
version: "3"
services:
easysearch-node1:
user: "602:602"
image: infinilabs/easysearch:1.11.1-2000
container_name: easysearch-node1
hostname: easysearch-node1
environment:
- "ES_JAVA_OPTS=-Xms1g -Xmx1g"
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536
hard: 65536
volumes:
- $PWD/ezs1/config:/app/easysearch/config
- $PWD/ezs1/data:/app/easysearch/data
- $PWD/ezs1/logs:/app/easysearch/logs
ports:
- 9201:9200
- 9301:9300
networks:
- esnet
easysearch-node2:
user: "602:602"
image: infinilabs/easysearch:1.11.1-2000
container_name: easysearch-node2
hostname: easysearch-node2
environment:
- "ES_JAVA_OPTS=-Xms1g -Xmx1g"
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536
hard: 65536
volumes:
- $PWD/ezs2/config:/app/easysearch/config
- $PWD/ezs2/data:/app/easysearch/data
- $PWD/ezs2/logs:/app/easysearch/logs
ports:
- 9202:9200
- 9302:9300
networks:
- esnet
console:
image: infinilabs/console:1.29.1-2000
container_name: console
hostname: console
volumes:
- $PWD/console/data:/data
- $PWD/console/log:/log
networks:
- esnet
ports:
- 9000:9000
links:
- easysearch-node1:es1
- easysearch-node2:es2
environment:
- TZ=Asia/Shanghai
networks:
esnet:
driver: bridge
ipam:
config:
- subnet: 172.24.0.0/16
1️⃣ 容器是否正常运行?
1
docker ps -a | grep easysearch
若容器状态为 Exited,说明启动失败。请查看容器日志进一步排查:
1
docker logs <容器名>
如果你看到了如下错误信息,而你使用的是自签证书,可以暂时忽略:
1
2
javax.net.ssl.SSLHandshakeException: Empty client certificate chain
javax.net.ssl.SSLHandshakeException: Received fatal alert: certificate_unknown
2️⃣ 容器是否监听端口?
进入容器内部查看:
1
2
docker exec -it <容器名> bash
netstat -tlnp
期望看到监听地址为 0.0.0.0:9200 和 0.0.0.0:9300,说明服务对外暴露成功。例如:
1
2
tcp 0 0 0.0.0.0:9200 0.0.0.0:* LISTEN 7/java
tcp 0 0 0.0.0.0:9300 0.0.0.0:* LISTEN 7/java
3️⃣ Docker 端口映射是否配置正确?
检查 docker-compose.yml 中 ports 映射是否正确,或者用以下命令查看实际映射情况:
1
docker ps
确认是否已将容器内部端口映射到宿主机。
宿主机上也可以通过 netstat 或 ss 命令查看端口监听:
1
netstat -tlnp | grep 9200
确保监听地址是 0.0.0.0:9200,而非 127.0.0.1。
-t:显示 TCP 连接
-l:仅显示监听状态(Listening)的端口
-n:以数字方式显示地址和端口(避免 DNS 解析)
-p:显示监听端口的程序 PID 和名称
4️⃣ 网络配置是否连通?
使用 curl 测试:
1
curl http://yourhost:9200
如需远程访问,需确认:
容器监听的是 0.0.0.0
映射端口已开放
网络桥接配置正常
🧩 二、非 Docker 部署场景的排查方法
1️⃣ 服务是否启动?
1
ps aux | grep easysearch
也可查看 nohup.out 或 logs 目录中的日志文件,看是否存在环境变量、路径错误、权限不足等问题。
2️⃣ 是否监听了正确的地址?
1
netstat -tulnp | grep java
EasySearch 默认只监听本地,建议修改配置文件:
1
2
3
# config/easysearch.yml
network.host: 0.0.0.0
http.port: 9200
3️⃣ 防火墙是否放行?
确认 Linux 主机的防火墙设置:
1
2
sudo ufw status
sudo iptables -L -n
确保目标端口(如 9200、9300)已允许外部访问。
🌐 三、通用排查项(适用于所有部署方式)
✅ 云服务器:检查安全组
云服务商(如 AWS、阿里云)通常还需配置安全组或防火墙规则,确保目标端口对外开放。
✅ DNS 设置是否正确
使用 dig 和 ping 测试域名解析与连通性:
1
2
dig +short yourdomain.com
ping yourdomain.com
可用 traceroute 进一步分析路径:(我在 MacOS 下测试的)
1
2
3
4
5
6
sudo traceroute -P TCP -p 9200 192.168.X.X
traceroute to 192.168.X.X (192.168.X.X), 64 hops max, 40 byte packets
1 192.168.X.X (192.168.X.X) 3.756 ms 3.208 ms 3.142 ms
✅ 总结:排查 EasySearch 的四步法
1
2
3
4
1. 服务是否启动?
2. 端口是否监听?
3. 网络是否打通?
4. 安全组是否放行?
无论是本地部署还是云端部署,掌握上述排查方法,你就能迅速定位并解决 EasySearch 无法访问的问题。
如果你觉得有帮助,也欢迎你将本文加入收藏夹,或转发给其他使用 EasySearch 的小伙伴 👇
---
## Easysearch 基础运维扫盲指南:从 HTTP 到 HTTPS、认证与安全访问全解析
- URL: https://blog.no-claw.com/posts/1f7436df/
- 日期: 2025-04-22
- 分类: 极限科技 / Easysearch
- 标签: 极限科技, 搜索引擎(ES)
众所周知,原生 Elasticsearch 默认开启的是 HTTP 明文接口,并且不开启认证或加密。如果要启用 TLS/SSL 加密和账号认证,通常需要额外配置一系列安全模块。
而在 EasySearch 中,官方通过 initialize.sh 初始化脚本,大大简化了这些安全配置,启动时就能自动生成证书并开启密码保护。不过在一些测试或开发环境中,我们可能希望临时使用 HTTP + 无密码 的简化方式来调试。
本文将从配置文件入手,逐步说明如何启用或关闭认证、如何从 HTTPS 切换回 HTTP,以及如何开放外网访问。
🛠 初始化启动信息与默认密码
当你执行 bin/initialize.sh 后,终端和 initialize.log 会输出初始化信息,其中包括自动生成的管理员账号密码,例如:
1
curl -ku admin:160824cab0b02193226e https://localhost:9200
默认情况下,服务已启用 HTTPS 与密码认证。
⚙ 修改配置文件:easysearch.yml
配置文件位于:
1
config/easysearch.yml
可以在此文件中自定义集群名称:
1
cluster.name: my-application
修改前访问效果如下:
1
curl -ku admin:160824cab0b02193226e http://localhost:9200
1
"cluster_name": "easysearch",
修改后再次访问:
1
curl -ku admin:160824cab0b02193226e http://localhost:9200
1
"cluster_name": "my-application",
🌐 开放外网访问(单节点部署)
如果你希望让其他设备或公网访问 EasySearch,可以添加以下参数:
1
2
3
network.host: 0.0.0.0
http.port: 9200
discovery.type: single-node
此外,为了提高对 Elasticsearch 客户端的兼容性,建议添加:
1
elasticsearch.api_compatibility: true
🔒 控制 HTTPS 与密码认证
EasySearch 的安全配置由以下两个核心参数控制:
参数
作用
security.enabled
是否启用认证(账号密码)和 HTTPS 模块
security.ssl.http.enabled
是否启用 HTTPS(SSL/TLS 加密)
🔁 配置组合含义如下:
security.enabled
security.ssl.http.enabled
效果
true
true
默认配置,启用认证 + HTTPS(推荐)
true
false
仅认证,无加密,使用明文 HTTP
false
true
无认证,HTTPS 加密,仅适合特殊用途
false
false
最开放,HTTP + 无密码,不推荐生产使用
你可以根据实际需求选择是否打开加密或认证,适配测试与生产环境。
📸 示例:启用 HTTP 且开启认证
如果你修改配置为 security.ssl.http.enabled: false,即可使用 HTTP,但仍要求输入用户名密码进行访问:
1
2
security.enabled: true
security.ssl.http.enabled: false
访问效果如下图所示:
✅ 小结
场景
推荐配置
本地测试(不加密、无密码)
security.enabled: false + security.ssl.http.enabled: false
本地测试(仅加认证)
security.enabled: true + security.ssl.http.enabled: false
安全访问(默认)
security.enabled: true + security.ssl.http.enabled: true
HTTPS 不认证
security.enabled: false + security.ssl.http.enabled: true
EasySearch 提供了灵活的配置方式,适合不同场景自由切换。对于开发者来说,理解这两个参数的作用,是快速上手运维的第一步。
提到认证,我们再看看如何修改密码,由于 Easysearch 默认新建了一个 admin 的用户,并且存在 config/security/user.yml 下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
---
# This is the internal user database create by initialize
# The hash value is a bcrypt hash and can be generated with hash_password.sh
_meta:
type: "user"
config_version: 2
# Define your internal users here
## Demo users
admin:
hash: "$2y$12$rmNDJxpQdRDb3F1dqk.uweSZqH3VAqeEpkP298vJ6QS99K80kbRoO"
reserved: true
external_roles:
- "admin"
description: "Admin user"
这个我们也可以使用 postman 开调用 API.http://localhost:9200/\_security/account
1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
url = "http://192.168.5.8:9200"
payload = {}
headers = {
'Authorization': 'Basic YWRtaW46MTYwODI0Y2FiMGIwMjE5MzIyNmU='
}
response = requests.request("GET", url, headers=headers, data=payload)
print(response.text)
新建用户
查看接口:https://localhost:9200/\_security/user
不过 yml 文件还是只有 admin,用 api 查看
使用新用户测试可以访问:
mysql -P 3306 -u admin -p -h mysqlsource.crtihcoeqzab.us-west-2.rds.amazonaws.com < people.sql
mysql -P 3306 -u admin -p -h mysqlsource.crtihcoeqzab.us-west-2.rds.amazonaws.com people
psql -h cloudacademylabs-targetcluster-0zdokkxbiyyh.cluster-ro-crtihcoeqzab.us-west-2.rds.amazonaws.com -U postgres -p 5432 people
---
## Coco-AI 接入 Google drive
- URL: https://blog.no-claw.com/posts/10b05aec/
- 日期: 2025-04-16
- 分类: 极限科技 / Coco
- 标签: Coco-AI, 极限科技
在 Coco-AI 最早的版本就提供了接入 Google drive 的视频,今天我终于实现了,而且借着全图形化的优势更加方便了。
参考这个文档新建 google SSO(好像也没啥参考性)
https://developers.google.com/workspace/drive/api/quickstart/go?hl=zh-cn
创建客户端
填入信息,
然后在数据访问中添加权限 -
缺少的权限加在这里(如图),https://www.googleapis.com/auth/drive
在 coco-sever 更新 google drive 的信息
然后在 coco-server 中连接
跳转 google sso
由于是测试账户,所以会有这个弹窗,继续就好
再次进行测试
获取权限
显示登陆成功
然后可以在数据源中看到对应数据
---
## Coco-AI 集成语雀作为语料库进行检索
- URL: https://blog.no-claw.com/posts/6a9ecc86/
- 日期: 2025-04-16
- 分类: 极限科技 / Coco
- 标签: Coco-AI, 极限科技
coco-AI 把语雀当作语料库做检索
说在前面,这个功能需要在语雀后台申请 Personal Access Token。使用的需要超级会员的(不是邀请新用户给的专业会员),所以需要付费使用。
然后在语雀后台,也就是https://www.yuque.com/settings/tokens处可以看见申请token的地方,如果你没有超级会员,这个是没办法用的。
点击新建,创建 token 分发权限,我这边给了所以的权限,语雀和 Notion 不同,这里给了权限就够了,其他地方无需在给权限。(手动@Notion 还要在文档或者文件夹授权)
点击查看详情可以看到 token,这里的 token 是可以反复查看的,由此语雀这一侧的设置完毕。
回到 coco-AI,我这边使用的是这个镜像,这里添加了对个人版本语雀的支持。
1
infinilabs/coco:0.3.2_NIGHTLY-20250417
启动命令如下:
1
docker run -d --name cocoserver -p 9000:9000 infinilabs/coco:0.3.2_NIGHTLY-20250417
然后进入后台初始化模型,我这里使用的本地部署的 deepseek:
点击数据源,选择 yuque connector
配置的地方很简单,填入数据源名称和 Token 和刷新时间。
然后我们就可以看到刷新的数据了
---
## Coco-AI 接入自定义数据源
- URL: https://blog.no-claw.com/posts/271ab503/
- 日期: 2025-04-16
- 分类: 极限科技 / Coco
- 标签: Coco-AI, 极限科技
Coco-AI 接入自定义数据源
之前使用 Hugo Connector 接入和 hexo 和任意 Markdown,后来官方也支持了对于任意数据源的支持,主要是开放了这个接口:
具体操作如下:
设置 - conntor - 新增,让输入名称和描述等信息,新建出来 conntor
然后我们就能在数据源上的页面上看到刚刚添加的了 Customize Connector 了
点开提示,给了一个 API
然后我们去创建 token,如图
我这边使用 Postman 进行设置
如果你的请求没有带 token,就是这样的。
转成代码的是这样的,当然也可以开发自己的 agent。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
import json
url = "http://localhost:9000/datasource/d00eeolvf2xxx/_doc"
payload = json.dumps({
"title": "I am just a Coco doc that you can search",
"summary": "Nothing but great start",
"content": "Coco is a unified private search engien that you can trust.",
"url": "http://coco.rs/",
"icon": "default"
})
headers = {
'Content-Type': 'application/json'
}
response = requests.request("POST", url, headers=headers, data=payload)
print(response.text)
---
## Coco-AI 支持嵌入,让你的网站拥有 AI 搜索力
- URL: https://blog.no-claw.com/posts/e32dbde5/
- 日期: 2025-04-02
- 分类: 极限科技 / Coco
- 标签: Coco-AI, 极限科技
Coco-AI 支持嵌入,让你的网站支持 AI 搜索
在之前的文章中,我们让 Hexo,hugo 博客 支持了 coco AI 检索,也就是说我们还得使用客户端来检索,那是不是把搜索放在博客上呢?
Coco-AI 在 0.3 的版本中
先找一个 html 来看个效果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
搜索组件嵌入示例
欢迎使用 Cloudsmithy 搜索组件
下面是通过 ES Module 引入的搜索框:
💡 原始代码:
1
2
3
4
5
🧩 每部分解析
1.
这是一个空的 div 元素,作为挂载容器。你的 searchbox 组件会被渲染进这个 div。
就像 React 或 Vue 应用会挂载到 一样
你在这里指定了 id 为 searchbox,用于后面初始化组件时绑定
2.
它就是在某个 div 中挂载一个聊天插件一样。你这段代码的本质就是嵌入并运行一个远程提供的 JS 组件。
✅ 总结一句话:
这段代码的作用是:
把远程加载的 searchbox 组件挂载到网页上的
中,完成动态插入功能。
如果你希望我反向帮你写出那个 searchbox 模块怎么写,或者做成本地文件测试,也可以告诉我~
下面是一个可以在任意网站自动嵌入你给出的
和