作者: nickzhuo

  • 偷窥人间:如何定义中国人

    从“宅兹中国”展说起

    每隔一段时间我都会去上博看看,上博地理位置优,人少,总之我很喜欢。上次去正巧碰到“宅兹中国——河南夏商周三代文明展”,展品都是来自河南省内发掘的夏、商、周三代重要考古发现。之所以这个展取名“宅兹中国”,就是为了纪念国宝何尊,上面所刻的“宅兹中国”(大意为我要住在天下的中央地区),这几个字是“中国”一词最早的文字记载。

    这个展大部分展品都是青铜器,但青铜器本身就是上博强项,所以并没有很吸引我。但看完这个展后,如何定义中国人这三个字带给我思考很多,今天记录下我的一点点简单的想法。

    夏的断代并不重要

    “宅兹中国”展进门就能看到二里头的模型,二里头被认为最有可能是夏朝的都城。当然这就涉及到一个很大的问题,夏朝是否存在?我们所学历史,夏朝是中国的第一个朝代,但因为没有任何夏朝的文字发现,目前尚未完成对夏朝的断代。

    我不是探究夏之断代的,也没有这个本事,因为在我心中夏朝是存在的,断代并不重要。我们认为夏朝的人居住在如今中国的版图上,因此认定是最早的中国人,但我在思考,中国人之所以是中国人就必须是建立在居住地点之上吗

    商朝文字让我们了解过去

    夏朝之后的朝代是商朝,“封神榜”的故事大部分来自于此。从商朝出土的文物来看,各种青铜器有了一个目的很明确且很重要的作用,那就是祭祀。在祭祀的过程中,会对活物进行杀戮,将敌人乃至同胞置于青铜器中煮熟。而在此过程中,还诞生了甲骨文,甲骨文是一个神奇的东西,正是从甲骨文、金文、大篆、小篆一直延续到今天的文字,中华文明对于文字系统的传承跨度在这个星球上独一无二的,但我在思考,中国人之所以是中国人就必须是建立在使用相同文字系统之上吗

    夏和商的迷思:他们真的和我们一样吗?

    先回答前面夏和商带来的问题,我百分百肯定我们是夏朝和商朝的后代,但是,他们和现代中国人没有双向之联系。祖先定居的地方决定了我们的生活习惯,甚至影响我们的基因。但那只是自然迁徙的一种选择,现代人因为交通便捷,很多华人生活在海外,他们仍然是华夏的种子。

    商朝人拥有一套现代人看上去极其血腥的管理模式,直白点,这种暴力统治模式更接近原始人。我们也无法去反对这种行为,因为文明进化的过程都是起伏的。

    显然现代中国人并不是以暴力为基准来匹配资源,商朝人是我们的祖先,但他们和地球上其他同时代文明一样,还在被活下去这个目的驱动着,所以武力是他们的第一选择顺位。所以退一步说,夏和商都是地球文明,虽有大同小异,但还没成为“中国人”

    伟大的开始:周公旦

    那中国人是什么时候成为“中国人”的呢?我以为既不是居住的位置,也不是文字系统传承,而是信仰的统一。我不得不提下周公旦,作为周王朝的第二代统治者,他的伟大在于用一套制度来规范了自己的子民,当然也约束了自己。他系统了解释了人和人之间的关系,以及遵循的规矩。尤其在文化上周公提出了“明德慎罚”的道德规范,制定了完整的礼仪仪式,此外周公曾提出“敬德保民”,制礼作乐,建立典章制度。

    用上帝视角来看,环顾此时地球上其他文明,周公旦并没有为了方便统治而创造出一个一神教或者多神教,他所恪守的规律始终建立在人上。我认为正是他的体系诞生了中国人这个概念,甚至可以说接受周公旦想法并且凝聚在一起的人,我们可以叫做中国人,当然此时的中国人概念还没有彻底完成。

    穿越时间的伟大:孔子

    谈孔子不得不提下他所处的时代以及他的阶级出身,孔子出生于春秋时期,此时中原大陆是一个大分裂状态。在这个礼乐崩坏背景下,周公旦设计的制度已经成为了摆设,统治者和老百姓成为了狼和羊的关系。此时的普通人是完全没有安生日子的,农作物收成即使好个几年,也不免卷入来年发动的战争中,这种环境简直就是人间炼狱。

    我喜欢仲尼,他是贵族出身,他本可以像其他勋贵一样完全为更高级的统治者服务,用自己的智慧鱼肉百姓。但他的伟大就在于他传播的思想站在了老百姓这一面,并且不停地去游说别人相信他的理论。他成天看到那些虚伪的人,一旦拥有了知识就开始欺负弱小,巧取豪夺老百姓的一切。仲尼看不惯这些垃圾,基于周公旦的思想,他提出了“仁”和“礼”的概念,提倡“富民”的理论。正是孔子对周公旦思想的提炼和强化,使得中国人的概念得到发芽生长。

    孔子的伟大是穿越时代的,同时期地球其他的文明还是没有能匹敌的对手,即使在仲尼死后五百年,西方才出了一个耶稣。虽然基督教包裹了很多好的、善良的、基于人的想法,但依然是一个一神教模型。

    上le眼药的独尊儒术

    孔子的思想是正确的且先进的,但在大分裂的背景下,不能被当时的统治者接受。好在中国最终迎来秦汉这个大一统时代,于是有了一个好消息,一个坏消息。好消息是,汉武帝刘彻提出罢黜百家,独尊儒术,坏消息是,这个儒术是上过眼药的版本。

    董仲舒将孔子作为招牌,把原版的儒学魔改成“三纲五常”和“天人感应”的唯皇帝论,将皇帝和老天联系在一起,添加了神秘主义,底层逻辑居然类似一神论,这是一种大退步。将最底层老百姓置于国家最不重要的位置,却反过来对他们却要求甚多。董仲舒倡导的儒学只有孔子这个名称作为空壳,他给孔子伟大的思想蒙上了尘埃,让本来已经领先地球其他文明两千年的思想发展开始停滞。

    不停的魔改,沦为工具

    当儒学成为显学后,他离皇帝更近了,离老百姓更远了。所有的东西都往上贴,有的好,有的不好,但都被包裹成儒学的一部分,只要被认为对皇帝的统治有利。

    到了魏晋时期,儒学已经被魔改的不成样子,成为了一个谁都无法轻松掌握的东西,包含了大量复杂的语言体系,他已经不能被一方势力当做话语权使用了。于是,儒学一度被玄学取代,东晋建立后,其特殊的门阀政治,让原来的儒学世家,纷纷改投玄学门下,摇身一变成为玄学士族。

    后来的后来,儒学已经不是原本孔子版本,彻彻底底的成为工具,这里就不铺开说了。

    思想的复兴

    欧洲在14世纪到16世纪发起了“文艺复兴运动”,当时的人们认为,文艺在希腊、罗马古典时代曾高度繁荣,但在中世纪“黑暗时代”却衰败湮没,直到14世纪后才获得“再生”与“复兴”,因此称为“文艺复兴”。

    中国人农耕习惯成型的早,思想习惯其实也成型的早,但可惜在历史进化过程中没有将其延续。所以我认为,我们要将先秦诸子百家的东西拿出来看看,好的东西留下来。把原版孔子拿出来,周公旦拿出来,和时代结合,精进中国人的思想,挖掘回原来的样子,来一次思想的复兴

    最后:“仁”和“礼”

    草草收个尾,快速总结一下,孔子思想体系的核心是“仁”和“礼”,德化社会的最高标准是“礼”,而德化人生的最高价值是“仁”。当我们能够做到这两点,就可以认为是中国人,因为我们的思想一致。

    所以,成为中国人不一定需要住在中国的土地上,也不用一样的文字。即使他是黑色的皮肤,不会说中国话,只要能够拥有“仁”和“礼”,我觉得他就可以被认为是中国人。但反过来说,如果“不够仁”,就算他土生土长在中国土地上,说着一口流利中国话,他也不是中国人。

  • 偷窥人间:健身房的潜规则

    问题概述

    健身失败成为了健身房主流

    走进健身房,通过锻炼来强化身体,是现在很多人正在做的事。但根据我的观察,表面上参与运动的人多了,健身失败却成为了主流,最具代表性质的便是想减肥之人瘦不下来。更严重的是,即使很多人咬牙坚持几个月甚至几年都没有明显改变,这是哪里出了问题呢?

    作为一个长期锻炼者,我发现,健身房里是有潜规则的,只有熟悉了这个潜规则才能改变身体成功。

    健身房的潜规则就是自学的潜规则

    本文基础观点承接自前次教育的话题,总结就是:最好的教育来自于传承。今天探讨的是,若无法通过传承途径前进,便无疑需要自学成才。以健身房来举例子,有两个原因,首先,这是一个很多人日常会接触到的学习模型。其次,我们这代人的父辈几乎没有健身房锻炼身体习惯,所以更适合用来谈自学。

    没有正确方向,消耗了最宝贵的资源。

    在学习的过程中,绝大多数多人会犯最多一个问题,方向不对。观察商健里的普通用户,很多人虽有目的,有的喜欢健美,有的喜欢健力,有的要健康,但实际操作却大相径庭。

    在这里举个例子,同样带有目的性地去完成一件事情,假设要去开一个餐厅。如果经营不善,可能三个月就歇业了,这是为什么?道理很简单,没有资金了。但如此简单的道理换成健身,很多人却觉得,因为不用花钱,只要投入时间即可,自己可以无限地试错下去。但恰恰相反,时间和金钱都只是一种资源,时间本质比金钱更宝贵。但健身失败的人无法意识到这一点,三个月身体没有变化而持续地坚持锻炼,仿佛只要坚持就能发生质变。很遗憾,只要走错了方向,只能走向失败并且损失资源。所以,你必须能够判断自己是否走在正确的方向上,身体不会说谎!

    甚至在这个“坚持”的背后,可能还会因为选择了“坚持”而沾沾自喜,配合互联网打卡功能或发朋友圈鼓励自己。但请不要忘记,我们求的是结果,而不是过程。打卡发圈终究是过程,身体的变化才是结果。甚至,当你拥有成功的结果后,你可以说,你天生如此,从来没进过健身房,因为本质来说健身房也只是一个工具。

    一个人闷头瞎几把练

    走入健身房伊始,作为一个小白,肯定会通过某些途径去学习。自然会在获取资讯的时候多看看相关内容,无论是主动还是被动的。很多人认为只要自己开始学了便是自学,但回到前面说的,还是要以结果为导向。

    现在互联网是何等发达,各种教学应有尽有,这里不说内容本身对错问题,为什么同样的动作你也做但是你无法获得同样的效果?人的身体结构是不同的,在实际生活中,绝大多数人是不能通过互联网学习健身成功的。

    举个不贴切的例子,如果学习只要简单地看看模仿下,那为什么学生还要去学校呢,所有人都在家自学不就好了。

    潜规则=认识自己+向上社交

    认识自己:一定要意识到自己知识有短板

    对付问题的第一步是必须意识到自己有知识短板,几乎百分之九十五的健身房失败者都会撞到这堵墙上。通过观察自己,观察别人在一段时间内的变化,确认自己知识结构的位置。意识到短板之后,才会主动通过外部知识体系补充。所以,只有意识到自己有问题才能前进,才会去主动找知识。

    向上社交:最快速获得知识途径

    社交的本质就是交易,这个交易未必用金钱来做介质。你需要从任何一个比你优秀的人这里学习他的优点,你应该很虚心的请教他,同时你也应该有能力去真诚的回报他,你可以通过自己的一技之长去和他交换他的知识结构。

    所有的技术都包含大量细节,很多时候我们不需要知道什么是对的,而是要知道什么是错的,大部分人只有通过纠错才能前进,而纠错的本质是通晓了原理才行。所以很多时候只有线下的教学才能成功,健身是一个很显性的东西,线下的教学纠错是你成功的关键。

    你有一个师傅是无比重要的事情,你如果有很多师傅那就再好不过了,当然所有的东西都要经过自身去验证,不用尽信。这里借用陈云同志和毛主席在延安窑洞的三问三答后的总结,“不唯上、不唯书、只唯实,交换、比较、反复”。

    最佳实践:BEST PRACTICE

    你应该抱着学习的态度来到健身房,你可以选择通过金钱获得私教的指导,也可以通过社交获得师傅的指导,但必须意识到你是需要指导的。在线下纠错模式下的学习效率远比你自己瞎搞来得快,前提是你必须要能甄别出优秀的师傅。

    结论

    最可悲的结论:时间并不存在

    沿着南京西路往东走,必然是外滩,同理,只要正确的方法锻炼,你就能获得正确结果。

    反例便是,你没事刷刷健身自媒体,认为自己很努力地在学习,甚至有些自鸣得意。到了健身房没有计划地随便练一练,甚至没有一个动作是正确的。你完全不懂训练,饮食,解刨,训练时你只和相同水平的人交流,聊天浪费了你很多时间。此时你想通过时间的积累来获得成功,很遗憾,这个时候时间不存在,即使你能活到五百岁也不太行。

    最高兴的结论:成功是可以复制的

    如果能通过自己的学习在健身房锻炼成功,那证明你会自学了,而这个模型是可以复制的。你完全理解怎么观察自己,怎么寻找帮助,怎么精进,你可以在其他的领域一样成功起来,你的成功可以复制。换言之,职场也是一个学习赛道,你如果能意识到自己的位置和向上社交,才能获得成功。

    最灰暗的结论:回到传承

    当你的成功模式可以复制,就如同本文健身的例子,最显著的结果就是你的下一代不会有身体锻炼的困惑,因为你可以指导他,并且传授学习的方法。否则下一代依然会面临健身房模型,如果撞墙则又需要一代人的努力,这才是最可怕的。试想一下,为什么肥胖会伴随家族遗传,他遗传的到底是什么呢?真的只是基因那么简单?

    额外的:不要好为人师

    或许你认为我会建议大家要成为师傅,但很遗憾这不是我的观点。当你拥有好东西的时候你确实很希望分享,但作为正经学习这件事情上需要三思,不要去好为人师来满足自己的虚荣心。

    如果对方尚未意识到自己存在问题,千万不要去纠错别人,即使你是善意的。因为在实际生活中往往别人会觉得反感,即使一次成功的教学也不能改变整个学习轨迹,只有他自己意识到有学习的渴望,才有可能成功。

  • WIN10下HDR设置让人捉摸不透

    和MAC直接用相比,WIN下的HDR有点琢磨不透,我的显示器是KOIOS 3221UD,播放器是POTPLAYER。之前因为都是维持着MAC下设置,看HDR视频总是发暗,经过不懈努力我终于发现。

    • 保持WIN显示设置HDR开启状态
    • KOIOS设置里HDR选项设置成自动(这点很重要)
    • 如果用了madvr可以在显示器hdr设置中选择第二项,让hdr信号直接输出到显示器。
    • 如果用N卡,控制面板中视频项颜色由播放器决定,不要覆盖了播放器选项。
  • 上海的秋天终于到来了

    刚刚过去的上海夏天的温度应该是历年体感最高的,黄梅天都没能成功占领这个城市,每天的白天都被40多的温度占领,到了午夜时分,也是36,37能让人中暑的温度,上海的夏天越来越难过了。

    秋天终于来了,虽然还处于不稳定状态,但是大自然让身体是瞬间舒服起来,人的活力也被激发了,白天的户外活动也开始解锁。

  • macOS Monterey 12.5更新终于把外接HDR显示器重启后没有HDR的bug修复了

    只要是外接HDR显示器,只要系统没有重启,是从睡眠中唤醒的,那么在youtube中的HDR选项就会没有,这时候只有重启或者重新插拔DP线。现在12.5更新了,唤醒后再youtube中也有HDR选项了,方便了。

    再次测试了下,bug还在,重启后外接显示器还是会丢失youtube里hdr选项。

  • 尝试使用高德官方flutter插件amap_flutter_map

    之前一直在用amap_map_fluttify ,但我发现国内开源太容易半途而废了,当然不去说对错,这东东算是完了,就算以后再发新版本我也不敢用了。

    高德官方的flutter插件早就出了,这次小试了下,感觉不错。

    • 支持Null-Safety和新版本flutter完美兼容。
    • 配置方便,可以在native端配置key,也可以直接在代码中配置。
    • 最新版本完成了工信部的合规!这个很重要!
    • 官方版本,更新更及时,可以单独配置SDK版本。

    相较于以前版本的复杂配置,高德官方版本配置很方便,flutter可以搭配不同的native端!只要在android/app/build.gradle配置,就可以选择你要的SDK版本。

    dependencies {
        implementation('com.amap.api:3dmap:9.2.1')
        implementation('com.amap.api:location:6.1.0')
    }

    就可以完美使用了,不用自己去加载lib和so,很方便,当然缺点也有。

    • 高德对于商用授权都要收费了,没有免费这一说了。
    • 只有地图和定位两大类包,不过这个也没关系,可以使用web api获得其他接口。
    • 没有官方讨论区,也只有商业授权用户才能得到官方的技术支持。
  • WordPress升级到了6.0,苹果换了DP线开启了HDR

    打开wordpress后台一看,奥哟,升级6.0了,果断升级,没有感觉出任何区别。

    最重要的事情记录一笔,之前macbook pro用的是hdmi,由于是hdmi 2.0在mac osx下是无法开启hdr的,所以换了跟display port线,dp 1.4版本,果然在系统偏好设置中,显示器设置中可以看到高动态范围这个选项了!太棒了,内置的radean pro 4gb看4k hdr毫无压力!非常棒!ATI其实有点被低估了!

    ps:DP外接显示器后务必安装一个MonitorControl,免费开源的工具,通过DCC调节显示器亮度对比度音量等。

  • ASUS的MB169C+必须关闭VividPixel

    老夫有一台便携屏华硕的MB169C+,但利用率不是很高,出差接苹果用的。之所以利用率不高,就是敲代码时候字体太丑陋。上海疫情期间,台机的1060挂了,迁移到MACBOOK PRO敲代码,又接上了我的便携屏。

    仔细研究了下,是华硕VividPixel这个技术在作怪,为了让边缘轮廓显示更锐利。这就奇了怪了,明明是一台用来办公用的屏幕,默认开启这种游戏配置很糟心,文本和图片显示都很诡异。所以,正确的办法就是关闭VividPixel,关闭TraceFree回归到一个正常显示文本的正确模式!一切完美!

    最后,这台ASUS还是有一个糟心的地方,不能连接switch!

  • 关于Dart中generic function type aliases衍生的一个问题

    之前Dart里定义一个函数结构可以用

    typedef Widget ItemGenerator<T> ({Key? key, required T model});

    这样的语法,但是到了新版本Dart静态检查会建议使用

    typedef ItemGenerator<T> = Widget Function({Key? key, required T model});

    这就会产生我今天犯下一个隐蔽的bug,引入泛型的时候会不小心写成。

    typedef ItemGenerator = Widget Function<T>({Key? key, required T model});

    能编译通过,但是在实际场景中你无法为你的变量设置正确的泛型,虽然看上去好像是正确的,补充一句,新的写法可以不单单定义回调,还可以定义一个结构,例如:经常用到的JSON格式在Dart中的定义,使其更具语义化!

    typedef JsonMap = Map<String, dynamic>;
  • Android Studio下Flutter项目控制台中文乱码

    AS构建Flutter项目出控制台中文乱码,之前都是修改android/app/build.gradle,添加:

    tasks.withType(JavaCompile) {
        options.encoding = "UTF-8"
    }

    但是这次玄学了,怎么restart清缓存都没用,最后参考 https://blog.csdn.net/u011054333/article/details/54175641 使用终极方法,在Windows下,新建GRADLE_OPTS环境变量,值为-Dfile.encoding=utf-8。然后新开一个终端窗口再次使用gradle命令,就会发现这下Gradle已经可以正确识别编码了。

    题外话,升级到了JDK18后我的IDEA也出中文乱码了,我惊了,不是18原生支持UTF-8么,只能回到JDK17

  • 舟哥的Spring Boot手册:Spring Security入门

    舟哥的Spring Boot手册

    目的

    • 旨在记录Spring Boot的经验
    • 从零到一构建WEB项目
    • 纯API服务

    资源

    • 基于Spring Boot 2.0构建
    • 推荐使用JetBrains IDEA编辑器

    完成度

    • [x] Spring Security
    • [x] Spring JPA

    简单介绍一下Spring Security

    Spring Security是Spring全家桶中负责安全的模块,安全是任何一个应用访问的基础。在我经验中安全主要提供两个主要功能,一个是Authentication,另外一个是Authorization。

    Authentication的意思是鉴权,也就是通过请求判断用户身份,传统的手段很多,比如表单登陆,Basic Auth,令牌等等。Authorization就是访问控制,用来控制应用内的ACL(Access Control List,访问控制列表),由此判断当前用户是否有权限来使用对应资源。

    Spring Security的设计初衷是所有应用都能使用安全模块,但毫无疑问作为全家桶成员,他在WEB方面是有很多天生优势的,和很多安全系统设计Shiro,Symfony Securiy等是高度相似的,如果有这些产品的经验,会很容易的转换到Spring Security中去。

    JWT概念

    概述

    JWT(JSON Web Token),是JWT组织提出的一种基于JSON在客户端和服务器之间交换用户态的一种规范。

    优点

    • 天生WEB亲和力
    • 不再使用Session,降低服务器性能开销。
    • 使用方便,全平台类库很容易找到。
    • 基于JWT可以衍生出很多安全策略

    微信登陆流程

    所有OPENID登陆基类

    package com.nuspet.yihuan.security.mini;
    
    import org.springframework.security.authentication.AbstractAuthenticationToken;
    
    // 小程序系列的token接口
    // 衍生出微信,百度,支付宝等小程序token的类
    public abstract class OpenIDToken extends AbstractAuthenticationToken {
    
        // 构造函数的说
        // 小程序类型的登陆token没有权限
        public OpenIDToken() {
            super(null);
            super.setAuthenticated(false);
        }
    
        // 小程序token都会转换成jwt token 所以不能设置信任为ture
        @Override
        public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
            if (isAuthenticated) {
                throw new IllegalArgumentException(
                        "不能设置小程序类token受信任,最终会转换成信任的jwt token。");
            }
            super.setAuthenticated(false);
        }
    }
  • 我的Mysql DockerCompose配置带my.cnf和自动定时备份

    一鼓作气

    把K8S拆了后,一鼓作气,MySQL也自建了一个,放放BLOG太实惠,就算本地用TCP不用套接字连,也比腾讯云的速度快太多。

    docker-compose.yml

    • 一共三个Service,mysql和mysql-backup以及PhpMyAdmin的管理界面,其中PMA没有做端口映射,需要访问你可以自己用端口或者Traefik之类代理把服务暴露出来。
    • my.cnf的权限:因为MySQL官方的要求,你需要在windows系统下把文件选择只读然后保存,在linux下chmod 444 my.cnf来修改权限防止world writeable的问题。
    • mysql-backup的备份目录权限:因为mysql-backup的代码是以appuser的权限执行,所以你要么让容器以root的权限执行,要么和我一样给/backup目录777就可以了。
    • backup启动时间要有空格,源代码localtime$target_time=$(date --date="${today}${DB_DUMP_BEGIN}" +"%s")兼容性问题。
    # 数据库配置 mysql+backup+phpmyadmin
    version: '3.4'
    services:
      mysql:
        image: mysql:5.7
        restart: always
        environment:
          - MYSQL_ROOT_PASSWORD=111111
        volumes:
          - ./my.cnf:/etc/mysql/conf.d/my.cnf # 额外的配置文件
          - ./data:/var/lib/mysql # 挂载data文件
          - ./backup:/backup # 挂载备份目录
          - /etc/localtime:/etc/localtime:ro # 传时间过去
        networks:
          - mysql
      mysql-backup:
        image: databack/mysql-backup:latest
        restart: always
        depends_on:
          - mysql
        environment:
          - DB_SERVER=mysql
          - DB_PORT=3306
          - DB_USER=root
          - DB_PASS=111111
          - DB_DUMP_TARGET=/backup
          # 注意时间要有空格
          - DB_DUMP_BEGIN= 0345
          - DB_DUMP_FREQ=1440
          # 分schema备份
          - DB_DUMP_BY_SCHEMA=true
          - COMPRESSION=bzip2
          - NICE=true
        networks:
          - mysql
        volumes:
          - /etc/localtime:/etc/localtime:ro # 传时间过去
          - ./backup:/backup # 挂载备份目录
      phpmyadmin:
        image: phpmyadmin:latest
        restart: always
        depends_on:
          - mysql
        networks:
          - traefik
          - mysql
        environment:
          - MYSQL_ROOT_PASSWORD=111111 # root密码
          - PMA_HOST=mysql
          - UPLOAD_LIMIT=300M # 上传限制
          #- PMA_ARBITRARY=1 # 可以使用任意服务器
    networks:
      traefik:
        external: true
      mysql:
        external: true

    my.cnf

    • 这个配置差不多是针对2核4G内存服务器配置的一个优化
    [client]
    default-character-set=utf8mb4
    
    [mysql]
    default-character-set=utf8mb4
    
    [mysqld]
    datadir=/var/lib/mysql
    server-id=1
    skip_ssl
    auto_increment_increment=1
    auto_increment_offset=1
    automatic_sp_privileges=ON
    back_log=3000
    log-bin=mysql-bin
    binlog_cache_size=2097152
    binlog_checksum=CRC32
    binlog_format=ROW
    binlog_order_commits=ON
    binlog_row_image=FULL
    binlog_rows_query_log_events=OFF
    binlog_stmt_cache_size=32768
    block_encryption_mode=AES-128-ECB
    bulk_insert_buffer_size=8388608
    character_set_filesystem=BINARY
    character_set_server=utf8mb4
    collation-server=utf8mb4_general_ci
    concurrent_insert=AUTO
    connect_timeout=10
    default_password_lifetime=0
    default_storage_engine=INNODB
    default_week_format=0
    delay_key_write=ON
    delayed_insert_limit=100
    delayed_insert_timeout=300
    delayed_queue_size=1000
    disconnect_on_expired_password=ON
    div_precision_increment=4
    end_markers_in_json=OFF
    eq_range_index_dive_limit=200
    event_scheduler=OFF
    explicit_defaults_for_timestamp=OFF
    flush_time=0
    ft_max_word_len=84
    ft_min_word_len=4
    ft_query_expansion_limit=20
    group_concat_max_len=1024
    host_cache_size=644
    init_connect=
    innodb_adaptive_flushing=ON
    innodb_adaptive_flushing_lwm=10
    innodb_adaptive_hash_index=OFF
    innodb_adaptive_max_sleep_delay=150000
    innodb_autoextend_increment=64
    innodb_autoinc_lock_mode=2
    innodb_buffer_pool_dump_at_shutdown=ON
    innodb_buffer_pool_dump_pct=25
    innodb_buffer_pool_instances=1
    innodb_buffer_pool_load_at_startup=ON
    innodb_buffer_pool_size=805306368
    innodb_change_buffer_max_size=25
    innodb_change_buffering=ALL
    innodb_checksum_algorithm=CRC32
    innodb_cmp_per_index_enabled=OFF
    innodb_commit_concurrency=0
    innodb_compression_failure_threshold_pct=5
    innodb_compression_level=6
    innodb_compression_pad_pct_max=50
    innodb_concurrency_tickets=5000
    innodb_deadlock_detect=ON
    innodb_default_row_format=DYNAMIC
    innodb_disable_sort_file_cache=OFF
    innodb_flush_log_at_trx_commit=1
    innodb_flush_method=O_DIRECT
    innodb_flush_neighbors=0
    innodb_flush_sync=ON
    innodb_ft_cache_size=8000000
    innodb_ft_enable_diag_print=OFF
    innodb_ft_enable_stopword=ON
    innodb_ft_max_token_size=84
    innodb_ft_min_token_size=3
    innodb_ft_num_word_optimize=2000
    innodb_ft_result_cache_limit=2000000000
    innodb_ft_server_stopword_table=NULL
    innodb_ft_sort_pll_degree=2
    innodb_ft_total_cache_size=640000000
    innodb_ft_user_stopword_table=NULL
    innodb_io_capacity=20000
    innodb_io_capacity_max=40000
    innodb_large_prefix=ON
    innodb_lock_wait_timeout=50
    innodb_log_checksums=ON
    innodb_log_compressed_pages=ON
    innodb_lru_scan_depth=1024
    innodb_max_dirty_pages_pct=75
    innodb_max_dirty_pages_pct_lwm=0
    innodb_max_purge_lag=0
    innodb_max_purge_lag_delay=0
    innodb_max_undo_log_size=1073741824
    innodb_monitor_disable=ALL
    innodb_monitor_enable=ALL
    innodb_old_blocks_pct=37
    innodb_old_blocks_time=1000
    innodb_online_alter_log_max_size=134217728
    innodb_optimize_fulltext_only=OFF
    innodb_page_cleaners=4
    innodb_print_all_deadlocks=ON
    innodb_purge_batch_size=300
    innodb_purge_rseg_truncate_frequency=128
    innodb_purge_threads=4
    innodb_random_read_ahead=OFF
    innodb_read_ahead_threshold=56
    innodb_read_io_threads=8
    innodb_rollback_on_timeout=OFF
    innodb_rollback_segments=128
    innodb_sort_buffer_size=1048576
    innodb_spin_wait_delay=6
    innodb_stats_auto_recalc=ON
    innodb_stats_method=NULLS_EQUAL
    innodb_stats_on_metadata=OFF
    innodb_stats_persistent=ON
    innodb_stats_persistent_sample_pages=20
    innodb_stats_transient_sample_pages=8
    innodb_status_output=OFF
    innodb_status_output_locks=OFF
    innodb_strict_mode=ON
    innodb_sync_array_size=1
    innodb_sync_spin_loops=30
    innodb_table_locks=ON
    innodb_thread_concurrency=0
    innodb_write_io_threads=8
    interactive_timeout=7200
    join_buffer_size=262144
    key_cache_age_threshold=300
    key_cache_block_size=1024
    key_cache_division_limit=100
    lc_time_names=EN_US
    local_infile=OFF
    lock_wait_timeout=31536000
    log_output=FILE
    log_queries_not_using_indexes=OFF
    log_slow_admin_statements=OFF
    log_throttle_queries_not_using_indexes=0
    log_timestamps=SYSTEM
    log_error_verbosity=3
    long_query_time=1
    low_priority_updates=OFF
    lower_case_table_names=1
    master_verify_checksum=OFF
    max_allowed_packet=1073741824
    max_connect_errors=999999999
    max_connections=1000
    max_error_count=64
    max_heap_table_size=67108864
    max_length_for_sort_data=1024
    max_points_in_geometry=65536
    max_prepared_stmt_count=16382
    max_sort_length=1024
    max_sp_recursion_depth=0
    max_user_connections=0
    min_examined_row_limit=0
    myisam_sort_buffer_size=8388608
    mysql_native_password_proxy_users=OFF
    net_buffer_length=16384
    net_read_timeout=30
    net_retry_count=10
    net_write_timeout=60
    ngram_token_size=2
    optimizer_prune_level=1
    optimizer_search_depth=62
    optimizer_switch=INDEX_MERGE=ON,INDEX_MERGE_UNION=ON,INDEX_MERGE_SORT_UNION=ON,INDEX_MERGE_INTERSECTION=ON,ENGINE_CONDITION_PUSHDOWN=ON,INDEX_CONDITION_PUSHDOWN=ON,MRR=ON,MRR_COST_BASED=ON,BLOCK_NESTED_LOOP=ON,BATCHED_KEY_ACCESS=OFF,MATERIALIZATION=ON,SEMIJOIN=ON,LOOSESCAN=ON,FIRSTMATCH=ON,DUPLICATEWEEDOUT=ON,SUBQUERY_MATERIALIZATION_COST_BASED=ON,USE_INDEX_EXTENSIONS=ON,CONDITION_FANOUT_FILTER=ON,DERIVED_MERGE=ON
    optimizer_trace_limit=1
    optimizer_trace_max_mem_size=16384
    optimizer_trace_offset=-1
    performance_schema=OFF
    preload_buffer_size=32768
    query_alloc_block_size=8192
    query_cache_limit=1048576
    query_cache_min_res_unit=4096
    query_cache_size=0
    query_cache_type=OFF
    query_cache_wlock_invalidate=OFF
    query_prealloc_size=8192
    range_alloc_block_size=4096
    range_optimizer_max_mem_size=8388608
    read_buffer_size=262144
    read_rnd_buffer_size=524288
    session_track_gtids=OFF
    session_track_schema=ON
    session_track_state_change=OFF
    sha256_password_proxy_users=OFF
    show_compatibility_56=OFF
    slave_net_timeout=120
    slave_parallel_type=LOGICAL_CLOCK
    slave_parallel_workers=0
    slave_rows_search_algorithms=TABLE_SCAN,INDEX_SCAN
    slow_launch_time=2
    slow_query_log=ON
    sort_buffer_size=868352
    sql_mode=ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
    stored_program_cache=256
    sync_binlog=1
    table_definition_cache=1024
    table_open_cache=1024
    table_open_cache_instances=4
    thread_cache_size=512
    thread_handling=one-thread-per-connection
    thread_stack=524288
    tmp_table_size=209715200
    transaction_alloc_block_size=8192
    transaction_prealloc_size=4096
    updatable_views_with_limit=YES
    wait_timeout=3600
  • 使用Fluentd收集容器内PHP-FPM下Monolog的日志

    K8S回归Docker Compose

    接上回说到,从K8S回归到Docker Compose,有一块调整的比较大,就是日志。原来有点懒惰,腾讯云的TKE集群可以默认接入他的LogListener,这个日志组件可以很方便从Container里取日志,方便到你根本不用改造原来的应用。但现在由于离开了K8S,还是要提升下收集日志的效率,所以所有容器内的应用都会由init进程管理输出到stdout或stderr,再由容器接住,新版本docker已经支持--log-driver=fluentd来指定一个fluentd的后端来接受所有日志。

    日志规划流向

    应用日志

    Monolog写php://stderr → PHP-FPM接到error_log → init进程Supervisor接到/dev/stderr → Container接到日志 → Docker daemon设置了log-driver → 写到Fluentd → Fluentd处理后通过Kafka协议发到腾讯云CLS日志处理

    访问日志

    因为Traefik不支持FastCGI协议,所以FPM前面是挡了一层Nginx,访问日志都从Nginx取,类似应用日志,不同的是访问日志都写了stdout,这其实并没有关系,因为最后在Fluentd处理的时候还是会用正则匹配的

    STEP0: NGINX ACCESS_LOG 访问日志

    在nginx.conf的http段中配置JSON格式的字段,根据你的需要来,我建议加入request_id,方便关联应用日志。

    http {
      log_format mylog escape=json '{"time_local":"$time_iso8601",'
                               '"request_id":"$request_id",'
                               '"host":"$server_addr",'
                               '"clientip":"$remote_addr",'
                               '"size":$body_bytes_sent,'
                               '"request":"$request",'
                               '"request_body":"$request_body",'
                               '"responsetime":$request_time,'
                               '"upstreamtime":"$upstream_response_time",'
                               '"http_host":"$host",'
                               '"url":"$uri",'
                               '"domain":"$host",'
                               '"xff":"$http_x_forwarded_for",'
                               '"referer":"$http_referer",'
                               '"http_user_agent":"$http_user_agent",'
                               '"status":"$status"}';
      include mime.types;
      server_tokens off;
      tcp_nopush on;
      tcp_nodelay on;
      keepalive_timeout 120;
      types_hash_max_size 2048;
      client_max_body_size 100m;
      default_type application/octet-stream;
      access_log off;
      error_log off;
      gzip on;
      gzip_disable "msie6";
      include vhost/*.conf;
    }

    在具体的虚拟机中就可以配置你设置的json格式access log

    server {
        server_name symfony.tld;
        root /data/www/public;
    
        location / {
            # try to serve file directly, fallback to index.php
            try_files $uri /index.php$is_args$args;
        }
    
        location ~ ^/index\.php(/|$) {
            fastcgi_pass unix:/dev/shm/php-fpm.sock;
            fastcgi_split_path_info ^(.+\.php)(/.*)$;
            include fastcgi_params;
    
            fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
            fastcgi_param DOCUMENT_ROOT $realpath_root;
            fastcgi_param HTTPS off;
            fastcgi_param REQUEST_ID $request_id;
    
            internal;
        }
    
        # return 404 for all other php files not matching the front controller
        # this prevents access to other php files you don't want to be accessible.
        location ~ \.php$ {
            return 404;
        }
    
        error_log stderr;
        access_log /dev/stdout mylog;
    }

    STEP1: 改造APP LOG

    App的日志一般都会用主流的包可以选择,在这个例子中,我用的是Symfony原配的Monolog,这里可以根据自己的需要进行拦截。例如访问量小的时候,你可以把所有用户侧的异常抓下来看看,所以app这个pool里,level是info的我都抓了。

    在你的异常拦截里面加上日志,可以从$_SERVER['REQUEST_ID']取得你需要的nginx侧请求ID。

    $this->logger->info($exception->getMessage(),[你需要的其他数据]);

    mololog的配置

    monolog:
        handlers:
            app:
                level: info
                type: stream
                path: "php://stderr"
                channels: [ app ]
                formatter: 'monolog.formatter.json'
            main:
                type: fingers_crossed
                action_level: error
                handler: nested
                excluded_404s:
                    # regex: exclude all 404 errors from the logs
                    - ^/
            nested:
                type: stream
                path: "php://stderr"
                formatter: 'monolog.formatter.json'
            console:
                type:   console
                process_psr_3_messages: false
                channels: ["!event", "!doctrine"]

    STEP2: 给Supervisor加上参数

    记得给启动命令加上参数--force-stderr --nodaemonize,否则PHP-FPM的/proc/pid/fd/2不是pipe,参考之前文章PHP-FPM在Docker没有日志输出到/dev/stder

    [program:php-fpm]
    command=/usr/local/sbin/php-fpm --force-stderr --nodaemonize --fpm-config /usr/local/etc/php-fpm.d/www.conf
    autostart=true
    autorestart=true
    priority=5
    stdout_events_enabled=true
    stderr_events_enabled=true
    stdout_logfile=/dev/stdout
    stdout_logfile_maxbytes=0
    stderr_logfile=/dev/stderr
    stderr_logfile_maxbytes=0
    stopsignal=QUIT

    STEP3: 修改容器日志输出驱动格式

    普通容器可以在run的时候加上--log-driver=fluentd,如果是compose可以在配置里加上logging字段。

    version: '3'
    
    services:
      delta:
        image: ccr.ccs.tencentyun.com/zhouzhou/phpallinone:allinone-master-20220331
        ports:
          - "80:80"
        logging:
          driver: "fluentd"
          options:
            fluentd-address: localhost:24224
            tag: delta
    networks:
      default:
        driver: bridge

    STEP4: 配置Fluentd

    Fluentd可以做很多事情,当然其他日志收集器也可以,但Docker内置了Fluentd驱动,就用了它。我使用的场景很简单,单纯的在宿主机进行收集,简单的格式化处理,由于日质体量很小,所以发送到腾讯云CLS来处理。

    发送到腾讯云CLS需要Kafka协议,所以我们要做一个自己的Fluentd镜像,加上了kafka协议和兼容腾讯侧的rdkafka库,请注意,github官网的debian镜像是有问题的,例子用的是Alpine版本。

    FROM fluentd:v1.14.0-1.0
    
    # Use root account to use apk
    USER root
    
    # below RUN includes plugin as examples elasticsearch is not required
    # you may customize including plugins as you wish
    # 安装rdkafka和kafka插件
    RUN apk add --no-cache --update --virtual .build-deps \
            sudo build-base ruby-dev bash\
     && sudo gem install rdkafka  \
     && sudo gem install fluent-plugin-kafka \
     && sudo gem sources --clear-all \
     && apk del .build-deps \
     && rm -rf /tmp/* /var/tmp/* /usr/lib/ruby/gems/*/cache/*.gem
    
    USER fluent
    
    # 覆盖一下
    ENTRYPOINT ["tini",  "--", "/bin/entrypoint.sh"]
    CMD ["fluentd"]

    配置这里说下思路,Fluentd的配置顺序简单来说是由上自下,并没有很强大的语法,利用本身的插件来完成一些其实本身很简单的操作,所以有点反直觉。我的容器中目前有访问日志和应用日志两类,我需要根据各自的特征发送到各自在腾讯云的主题进行分析。

    <source>
      @type forward
      port 24224
      bind 0.0.0.0
    </source>
    
    <match *>
      @type copy
      <store>
        @type relabel
        @label @PHPERROR
      </store>
      <store>
        @type relabel
        @label @NGINXACCESS
      </store>
    </match>
    
    <label @PHPERROR>
      <filter delta>
        @type grep
        <regexp>
          key log
          pattern \[pool ([^\]]*)\]
        </regexp>
      </filter>
    
      # 解析原始的PHP-FPM日志
      <filter delta>
        @type parser
        key_name log
        reserve_data true
        # reserve_data true
        # hash_value_field parsed
        <parse>
          @type regexp
          expression /^\[(?<logtime>[^\]]*)\] (?<level>(DEBUG|INFO|NOTICE|WARNING|ERROR|CRITICAL|ALERT|EMERGENCY)): \[pool (?<pool>[^\]]*)\] child (?<child>\d+) said into stderr: \"(?<message>.+)\"$/
        </parse>
      </filter>
    
      # 转json
      <filter delta>
        @type parser
        reserve_data true
        key_name message
        <parse>
          @type json
          time_key logtime
        </parse>
      </filter>
    
      <match delta>
        @type rdkafka2
    
        brokers sh-producer.cls.tencentcs.com:9096
        use_event_time true
    
        <format>
          @type json
        </format>
    
        <buffer topic>
          @type memory
          flush_interval 3s
        </buffer>
    
        # 腾讯云日志主题
        default_topic 9d7fc234-e090-4a48-8888-642f5b56e82c
    
        # producer settings
        required_acks -1
        rdkafka_delivery_handle_poll_timeout 5
    
        rdkafka_options {
          "security.protocol" : "SASL_PLAINTEXT",
          "sasl.mechanism" : "PLAIN",
          # 日志集主题
          "sasl.username" : "e63de52e-a012-45f6-8888-ca2dab7aeda1",
          "sasl.password" : "SecurityId#SecurityKey"
        }
      </match>
    
    </label>
    
    <label @NGINXACCESS>
      <filter delta>
        @type grep
        <regexp>
          key source
          pattern stdout
        </regexp>
      </filter>
    
      <filter delta>
        @type parser
        key_name log
        <parse>
          @type json
          # 指定nginx原始日志的事件作为事件事件
          time_key time_local
        </parse>
      </filter>
    
      <match delta>
        @type rdkafka2
    
        brokers sh-producer.cls.tencentcs.com:9096
        use_event_time true
    
        <format>
          @type json
        </format>
    
        <buffer topic>
          @type memory
          flush_interval 3s
        </buffer>
    
        # 腾讯云日志主题
        default_topic 0c0872c5-00b5-8888-b658-e88769118c30
    
        # producer settings
        required_acks -1
        rdkafka_delivery_handle_poll_timeout 5
    
        rdkafka_options {
          "security.protocol" : "SASL_PLAINTEXT",
          "sasl.mechanism" : "PLAIN",
          # 日志集主题
          "sasl.username" : "e63de52e-a012-8888-8932-ca2dab7aeda1",
          "sasl.password" : "SecurityId#SecurityKey"
        }
      </match>
    </label>

    以上配置简单的把单容器所有日志输出接住,先用relabel这个插件把日志复制出两股Label进行分别处理,这里有点反直觉。然后在不同的Label下进行json处理和投递工作,注意的是,PHP-FPM的Error LOG是不能定义格式的,所以需要正则匹配,然后把message部分进行json处理,注意使用reserve_data = true防止之前的key被覆写。最后记得腾讯云目前的Kafka环境必须用rdkafka库,SASL_PLAINTEXT密码的配置请注意!

    全部搞定

    请注意的时候,腾讯云的kafka可以走内网,域名和端口和外网的不同。你也可以自己做Elasticsearch和Kibana,组你自己的EFK

  • PHP-FPM在Docker没有日志输出到/dev/stderr

    kubernetes迁移产生日志收集问题

    今天把腾讯kubernetes TKE上的一些服务弄到单机docker-compose里去了。为什么迁TKE的问题先不谈,因为原来的服务日志都是依赖腾讯Loglistener,这个日志端和腾讯TKE是完美配合的,去node上拿文件日志非常方便,也没有去优化。但这次由于结构换了,期望从docker-compose的Fluentd驱动来获得日志然后用kafka协议发到接收端,第一步就是要把容器都输出到/dev/stdout和/dev/stderr。

    找不到日志

    monolog的配置

    代码用的是php里最有名的monolog,先改配置。把日志流输出到path: "php://stderr",这里有第一个发现,不能写path: "/dev/stderr"或者path: "/proc/1/fd/2",这两种写法都会报File Append的错误,显然要使用最前面的封装版本,当然这和手册上的有点出入。

    推荐你简单使用常量 STDIN、 STDOUT 和 STDERR 来代替手工打开这些封装器。

    https://www.php.net/manual/zh/wrappers.php.php 手册上原话。

    找不到日志

    容器服务是用Supervisor管理的,一个容器上跑php-fpm和nginx,Supervisor里FPM的配置是写到/dev/stderr里去的,但结果就是无论如何配置都无法写到/dev/stderr。

    bash-5.1# ps aux | grep master
        7 root      0:00 php-fpm: master process (/usr/local/etc/php-fpm.d/www.conf)
        8 root      0:00 nginx: master process /usr/local/nginx/sbin/nginx -g daemon off;

    查看下进程err输出情况。

    bash-5.1# ls -l /proc/7/fd/2
    l-wx------    1 root     root            64 Apr  7 14:44 /proc/7/fd/2 -> /usr/local/var/log/php-fpm.log

    日志被牢牢的写进了/usr/local/var/log/php-fpm.log,没有进管道,找了半天发现,是一个神奇的配置组合问题。

    [program:php-fpm]
    command=/usr/local/sbin/php-fpm --force-stderr --nodaemonize --fpm-config /usr/local/etc/php-fpm.d/www.conf
    autostart=true
    autorestart=true
    priority=5
    stdout_events_enabled=true
    stderr_events_enabled=true
    stdout_logfile=/dev/stdout
    stdout_logfile_maxbytes=0
    stderr_logfile=/dev/stderr
    stderr_logfile_maxbytes=0
    stopsignal=QUIT

    关键在于,当你用了nodaemonized,就要用–force-stderr来保证进程写到容器的stderr输出。参考,https://stackoverflow.com/questions/50995042/docker-does-not-catch-php-fpm-outputs-with-symfony-and-monolog

    As php-fpm is run in nodaemonized mode, you need to set the –force-stderr flag which "Force output to stderr in nodaemonize even if stderr is not a TTY."

    检查一下,连接对了!用docker logs -f container检查也没问题了!

    bash-5.1# ls -l /proc/7/fd/2
    l-wx------    1 root     root            64 Apr  7 14:44 /proc/7/fd/2 -> pipe:[13099270]
  • Traefik会修改头部以符合HTTP标准

    Traefik Modify Header

    使用Traefik代理的所有http请求,header的大小写会变化,这是因为Traefik用了Go的标准库,在这个库里所有的header都会遵循规范来。可以参考,https://github.com/golang/go/issues/5022

    Http Canonical 规范

    简单说,http 1.1对header是大小写敏感的,但http 2是header全小写。这样一来很多旧时的代码是无法兼容客户端变化的,例如:JWT需要使用header传递,你取key得时候就要根据http版本来取大小写的key。在大小写敏感前提下,authentication和Authentication代表着两个不同的key,但他们不应该同时存在!

    结论:Traefik的做法是对的

    我认为,服务端输出的header的代码应该是根据http版本输出大小写的,而客户端是知道自己的http版本而取对应的key。所以应该理解header大小写问题是一个抽象的概念,他只是抽象到头部中有一个key,我需要取得这个key,而在客户端中已经自动把对应版本的key进行了大小写转换。所以Traefik的做法是正确的,他把抽象的key根据协议转换了大小写。

    还是需要关心的问题

    大部分客户端和服务端并没有对http版本做对齐,所以你要关心你的开发环境如果不经过代理“标准化”头部是不是还正常,你的客户端能不能对齐服务端的变化!~