2024-08-21 21:10:00
本文的作者是费轩。
在黔追寻阳明行迹的六天里,最常听到的问题便是“阳明学对现代人的精神内耗能够起到什么作用?”其实,如果阳明只是一位古圣人,那么便无法对沦没于无聊日常生活的今人有什么借鉴意义,因为史诗、传奇的时代已经一去不复返了。然而,我却愿意将阳明视为我们的同时代人。王阳明在明代中期的政治 – 生存处境中,深刻地遭遇到个体性危机,作为一位敏锐的思想家和严肃的行动者,他以全副身心性命面对这一历史变局,阳明学便是这一历程的思想产物。
14 世纪末中国人口约六千五百万,至 1600 年已激增至一亿五千万左右。人口膨胀造成大量的冗余士人,他们虽然接受四书五经的经典教育,有着治国平天下的自我期许,但是无法通过科举的激烈竞争进入到国家政治体制之中,因此成为政治编外士人。里甲制趋于解体,被束缚在土地上的人口不断流动,进入到市镇当中。第一批耶稣会传教士的到来,使得新的天文、历法和算学知识,乃至不同以往的对宇宙的整体理解,在士人阶层中产生影响。明代政治的暴虐,君主的酷杀,以及由武宗开端的任性乱政或怠政,所有这一切,对“天地君亲师”的传统礼制都造成了深刻的影响。这一影响对于那些对时代敏感的士人而言,可以说是颠覆性的。阳明学正是在这样的环境中发生和发展。
阳明在少年时有着过人的精力,但他深思而敏感。阳明深思到什么程度?他格竹不成,于事事物物中穷理不得,竟然引发身体的病痛和精神的困顿。如果阳明到今天医院的精神科就诊,医生恐怕会将他诊断为抑郁状态吧!内在的激情过于旺盛,却无处释放,找不到人生的方向:他或是沉溺于兵法,出关逐胡儿骑射,或是沉溺于辞章之学,与诗友文人往来唱和,或在身为高官的父亲的庇佑下从事举业。以阳明的才智,他投身于其中任何一项事业,都会有所成就。但是,所有这些都使他感到不满足。他的肺病,简直可以说是内火郁积而来的。与那些编外士人相比,阳明是幸运的,因为他已经赢在了起跑线上,父亲王华为他早年的仕途费尽心力,甚至遭人非议。鲁迅《狂人日记》中的狂人,终于“候补上任”了,于是他的狂病不治而愈,回归“正常”。但是,步入仕途,只能让阳明的“精神病症”隐而不显,过了一段时间,工作又不能使他感到满足,于是修仙逃禅,归家养病。对于大多数获得编制的士人而言,休闲逃禅只是政治失意后的退路而已,所谓“退隐”,不过是暂时蛰伏起来,以逍遥的态度积蓄力量,一旦有合适的机会,便会毫不犹豫地重新回到政治。几乎对于所有中国士人而言,政治就是他们身心性命的全部,是他们此生此世的宗教信仰。
在龙场驿大酒店与友人夜谈,朋友说道:如果阳明在龙场没有悟道,那么阳明就死了。死的不是阳明的肉体——他终其一生都没有摆脱病痛的困扰,而是立志做圣人的那个青年阳明——阳明的精神。立志是无中生有的工夫。当他被贬至龙场,置身于荒蛮,处身于“赤身裸体”的无家可归状态时,“天地君亲师”的整全礼制随同洞穴的黑暗而归于隐没。所念只在“俟死”而已。
无中生有,就是向死而生。自心的觉悟,是照亮无明的炬火。这样的时刻,不是发生一次就可以许诺终生圆通的,在接下来的人生历程中,仍然会有困窘,仍然会面对初心退转的考验。
正德十四年平朱宸濠之乱,阳明对兵法的运用可谓出神入化,然而平乱后阳明陷入毁谤之中,甚至性命难保。其时阳明的精神状态,如他自述所言:终日恍恍,如在梦寐。避居杭州净慈寺养病也好,逃遁入九华山中也罢,阳明在这番考验中身心焦灼。在弟子蔡世新所绘阳明像中我们可以看到,平乱后的阳明面目瘦削、颧骨突出。他的友人也提到,平乱后,阳明鬓发斑白。
我们在这里看到的不是天生其德、高超圆融的自信自得,而是明确一己之有限性的戒慎恐惧。对于王阳明来说,自心觉悟是不断重临的考验,是在历史和时势当中不断激活的极端体验。自信觉悟不是一个如洒扫应对一样,可以欲求并求得,可以通过允执厥中的实践操练就能达至的一次性事件。这就是为什么在龙场悟道后多年,又有正德十四年和十五年的良知学突破。
听钱明老师讲,玩易窝在旅游开发之前,不仅是当地人的才采石场所,而且沦为了堆放垃圾的洞穴。今天展现在我们面前的玩易窝,是经过开掘与修整的。历史何其吊诡,对五百年来中国文化与士人精神产生如此巨大影响的阳明悟道地,竟然在几十年前以采石场和垃圾堆的形象存在,在今天又被纳入到旅游产业当中,成为人们拜谒古圣人的圣地。历史真是以戏剧性的方式向人们展现其自身的。
当明武宗沉迷于巡游享乐,不再亲祀天地,当明世宗在大礼议中以一己之私将本生父母列入帝后谱系,在在对“天地君亲师”的礼制秩序形成嘲讽与颠覆时,在专制君主的阴鸷酷杀和士大夫得君行道的期待中,在廷杖的屈辱与士大夫争相就杖并引以为豪的人性幽暗中,阳明和历史中的其他觉悟者们,难道没有觉出荒诞来吗?朱子说:“尧舜三王周公孔子所传之道,未尝一日得行于天地之间也。”历史的觉悟者并非对历史的非理性视若无睹,而是如阳明所说:“此念生于孩提,此念可去,是断灭种性矣。”正是这种不忍人之心,才使得深知历史 – 天命无常的觉悟者,以出世的精神做入世的工夫。
2024-05-30 09:21:08
本文的作者是费轩。
热气透过大敞的窗户涌入,我偏头看向内室的玻璃,觉得脸上干净不少。虽然瘦了,但是下巴显得分明。两道眉毛仍然紧凑在一起,不能舒展开来。宿舍内的书几乎已经搬空,每周返校工作,随身携带一些书,是韦伯和沃格林的。
就在这间屋子,还记得 2022 年刚搬进来时与 hl 畅聊、品茶、尝试咖啡;还记得那年底被封在宿舍楼中,担心不知何时被转运,蹲在卫生间手洗一大盆衣服;还记得 11 月底几乎每天到 hl 屋中激愤地诉说,看他一支烟接着一支烟地抽,然后沉默;还记得 2023 年 3 月的那次甲流,在屋中高烧两日,hl 送来生煎包和馄饨;更记得身心俱疲的 5 月,四五点钟天将要蒙蒙亮时,听着窗外传来的各种鸟叫,从未感到如此清晰、如此吵闹,想到那个人的拒绝和两个月的过往,咽下孤独,觉得喉咙发紧。
以前经常读到某位哲学家在自述中写到某年某月“大病,几死”。当时疑心是作者为了传奇色彩而夸张。有的人也许属于自我感觉不好,如李泽厚和熊十力,从中年开始就不断抱怨自己身体如何如何差,命不久矣,但却都很高寿。如果有幸忝列这一思想谱系,我在自述中大概也可以使用这句话,用来形容去年底的经历。23 年底到 24 年初厚厚的一沓检查报告和病历还留在文件夹中,按时序排列,原本想誊抄下时间节点,留下资料以便回顾,再做销毁。现在终于恢复了健康(甚至比大病之前还要健康),回顾也不想再做了,因为回忆使人痛苦,也因为精神已经松弛从而懒惰。这些病历很快就要去到它们该去的地方:碎纸机。
“西学为体中学为用,刚日读史柔日读经”,这幅联句还挂在老地方。22 年底每天醒来第一个看到的就是这幅联句,其时其境,可以说完全晓彻这两句话的含义,那时,我是以“取今复古,别立新宗”自认的。为什么总谈到一年半前已经“过去”了的事?难道是某种创伤记忆吗?难道就像那天给导师发了现场视频,老师秒回他在 35 年前的经历,一下触发了激情?衰颓固化的时代再也掀不起结构性变革的浪潮,个体自觉到与社会历史行程相切割,对生存处境的改善,只能乞灵于回到自己的一亩三分地,把小日过得精致到登峰造极,在各种分疏但却同一的体验中寻求认同,与世隔绝。这样,历史可真是终结了。
经此一遭,思想发生不少变化,当然,同心圆——那个回心之轴没有变。现在愈发明确,50 年一代学者(知青、文革一代)和在 50 年一代学者陶养下成长起来的 70 年一代学者(文化热一代,特别是在大学期间经历了 89 和后 89 的市场化浪潮),他们的思想不可能成为我精神解困的方法。90 一代在去政治化的环境中成长起来,这就决定了他们的非政治性(把去政治化说成也是一种政治,只不过反映出说者本身的政治性),使得他们面对 50、70 一代所未面对过的个体人生问题。出于智性上的清明,我不得不认为,这种个体人生问题,不是任何重新政治化的动能所能解决的,在当下,重新政治化的激情只能与已有建制和文化保守主义合流。90 一代很难像 70 一代那样受惠于 50 一代学者的思想(70 一代学者做的往往是把 50 一代学者思想精致化、国学化的工作),他们面对的是全新的问题,需要开辟全新的道路,在这方面,不能抱有任何便巧的幻想。
2024-03-24 14:36:37
本文最后更新于 2024 年 3 月 29 日
来自马来西亚的亚洲航空(AirAsia)近日推出了他们继 2022 年 SUPER+ 无限飞行年度计划之后的又一个年度订阅计划 AirAsia Unlimited Asean Pass(亚航东盟无限飞)。和上一年度的订阅计划不同,这一次的计划订阅费用变得更贵,并且仅限于在东盟国家的 69 条航线内的指定日期的航班内选择(要知道,去年的套餐是可以全球范围飞行的!)。然而抱着折腾不止的心态,我还是毅然决然购买了他们的年度飞行计划,踏上了一条折腾不止的不归路……
友情提示:这个计划本身有很多很坑的地方,如果没有做好折腾的准备,千万不要购买!
在亚行东盟无限飞计划的条款与条件页面中有诸多限制,这里先整理概括如下:
看起来是一个还算公平的计划,是这样吗?我算了一笔账,因为我居住在新加坡,并且持有的是中国内地护照,自疫情结束之后中国与东南亚许多国家都签订了互免签证协定。因此包括泰国、马来西亚与(将来很快的)印度尼西亚在内的许多国家,去旅行都会很方便。388 新币(约合 2000 人民币左右)的计划价格,只要平均每个月周末旅行一次,就可以完全值得回本。因此我义无反顾地购买了这项计划订阅。
我自认为完整阅读了计划的条款与条件后,便能清晰了解该计划的优缺点,于是购买计划后,我便开始预定机票,筹划着 5 月份的第一次旅行。然后我就遇到了这样的错误提示:
无法用于选定的航线或者日期?我又尝试了许多不同的目的地/时间选项,最后终于发现我忽略了无限飞订阅中的一个重要限制:所有的可用航班均以查询页面的展示为准,并不是除了限制日期外,所有日期都有航班,并且可用日期的航班也是有限的。以新加坡 – 吉隆坡的单程航班为例,可用于 2024 年 5 月 3 日(星期五)的预定航班如下:
新加坡 – 吉隆坡是全球最繁忙的国际航线,仅亚洲航空每天便有超过 12 班定期航班往返两座城市之间。而适用于无限飞的航班却选择“十分有限”。另外,该计划在许多目的地之间的限制,使“周末游”变得不是十分可行,以新加坡往返兰卡威的航班为例,2024 年 5 月 – 6 月期间,无限飞适用于如下日期:
因此,从新加坡到兰卡威的周末游变得就不是十分可行。AirAsia 官网上提供的查询工具非常简陋,仅能查询特定月份的两个目的地之间的单程航班的一般供应量情况:
抱着折腾不止的心态,我找到了该工具的数据源[Wayback Machine]:这是一个记录了从 2024 年 5 月 – 2025 年 4 月期间,无限飞计划的所有可用航班及其供应量的列表。值得注意的是,似乎该列表是动态增长的。在本文初次写就时,该列表中共有 17955 项可用的航班信息,而如今列表中拥有超过 2.1 万条航班信息数据。我对该数据进行了一些统计,发现一些有趣的数据事实如下:
import re
import json
from typing import NamedTuple, List, Dict
class Flight(NamedTuple):
Origin_Country_Name: str
Destination_Country_Name: str
Origin_Airport_NameAndCode: str
Destination_Airport_NameAndCode: str
DepartureDateMYT: str
TrafficLightSequence: str
class FlightRoute(NamedTuple):
origin_code: str
destination_code: str
def get_airport_code(string: str) -> str:
return re.findall(r'\(([A-Z]{3})\)', string)[0]
with open("O2availableseat.json", "r") as _f:
flights: List[Flight] = json.loads(_f.read())
flight_info: Dict[tuple, list] = {}
for flight in flights:
origin_code = get_airport_code(flight["Origin_Airport_NameAndCode"])
destination_code = get_airport_code(flight["Destination_Airport_NameAndCode"])
route = (origin_code, destination_code)
if route not in flight_info:
flight_info[route] = []
flight_info[route].append(flight)
flight_info = dict(sorted(flight_info.items(), key=lambda x: len(x[1]), reverse=True)) # sort by number of flights DESC
如果你需要更直观的查询这些数据,我已经将它共享到 Google Sheet 在线文档中,你可以随时浏览或将这些数据下载到本地。你可以使用这些数据来规划你的行程,比如说找到可以供周末旅游的航线,或者研究是否值得购买亚航无限飞。
2024-01-27 23:43:26
年终总结可能会迟到,但它从来不会缺席。
在过去的一年里,我和 Minda 探索了许多地方:新加坡、马来西亚、香港、马尔代夫、上海、布里斯班、北京。这比我过往任何一年的经历都要丰富。有些地方是之前从没来过,全新探索的。而另一些则是故地重游,顺便感受一下发展的新气象。而在旅途中时常让我感动的,其实大概率不是风景,而是人。有幸能够跟各种人交流,倾听他们的故事,近距离又真实地感受他们的情绪,走近他们的世界,这时常让我在内心热泪盈眶。
在过去的一年中,我很忙。有很多事情需要忙。大多数是工作上的,但也有少数不是。现在回头来看的话,我只能侥幸地说做得绝大多数事情都至少有一些成果,都在向前推进。但是这里也暴露出一个问题,就是我很难像之前一样,轻易有大块的不受干扰的集中注意力的时间(就像今天在撰写年终总结一样)来做我想要做的事情:很多事情交织在一起、相互穿插,一不小心就成了线团。有的时候我在想,为什么会变成这样。现在想来这应该是工作内容、外界干扰、个人心态多方面混合的结果。我从来没有想过专注时间会变成一项个人的奢侈品,我在过去一年也没有意识到这对我有那么重要,因为我从来没有应付过如今这么多事情。现在看来,保持平和的心境、切换环境、维持足够的休息和理智对我来说都很重要。这是我在 2024 年争取能够做到的。
这一年学到的另外一点是信守承诺——承诺不仅包含要完成哪些事情,更重要的是何时完成。时刻保持跟踪并维持高质量的交付始终是一件具有挑战性的事情。正如李显龙(Lee Hsien Loong)在 2019 年新加坡国庆群众普通话演讲中,引用芬兰石油公司纳斯特(Neste)总裁的一句话:“世界上最宝贵的资源是信用。要赢得别人的信任,我们必须兑现承诺,并付出更大的努力去守护这份信用。”(这里附上这段话的英文翻译,来体会这一点:The most valuable resource in the world is trust. But to find trust one must first earn it. And to keep trust, one must continue to earn it.)
某种意义上讲,周末对我来说是另一种形式的工作日。在我意识到这一点之后,适当的休假对我就变得尤其重要。还记得在电影《借刀杀人》(Collateral,台译 “落日殺神”)中,驾驶着黄色出租车的洛杉矶出租车司机麦克斯(Max)的驾驶座遮阳板里,就别着一张马尔代夫的照片。在电影中,这位出租车司机说道:“每次当我看到照片的时候,我就像是在度假”。
休假是一种逃避。在几分钟甚至几天的时间里,远离压力源,避免干扰,高质量地享受难得的宁静时光。你可能很难安排出来时间去制造假期,但这是百分之百值得的。当你必须陷入无穷无尽的交付陷阱时,给自己充足的时间去缓解压力,调整心态,是十分重要的。从这个角度来看,人类一生有限的时间本来也是另一样“奢侈品”啊。
Dota 2 中有一样游戏内道具叫做 “回音战刃”(Echo Sabre),能够在短时间内对敌人进行两次连续攻击。我的旅游习惯也是这样:去了一个好玩的地方,只要感受不错,就一定会有第二次。这一年里,香港、马尔代夫、澳大利亚都是我觉得可以再去的地方。事实上,其中有一些地方已经在我的 2024 年旅游计划中了。
有很多事情是牵扯平衡的艺术。这一年我们同事之间流行起了一款名为务农非易事(Agricola)的棋盘游戏(Board game),我和 Minda 私下也非常爱玩。这款游戏的核心逻辑就是平衡发展:大多数资源的累积都可以使玩家获得分数,但某种资源累积到太多量了之后,即使再累积更多资源,玩家也不会获得更多分数。我之前不怎么接触这一类的游戏,而无非就是在电脑上沉迷一些沙盒模拟器类的游戏。但在 《务农非易事》中,游戏分数却狠狠打击了我这种 “沉迷于某一些特定方面” 的玩家。其实通过玩这些游戏,你甚至能够体会总结出一些做事情的一般性的道理来。
还有一种平衡是平衡目标与预期。我也常翻看他人的年终总结博客文章,有一些人热衷于 “OKR 式量化地” 总结并且回顾上一年自己的 “小目标” 是否达成。我也会有一些年度目标,但是我并不希望量化它们。只希望在来年的年终总结中,这些方面会做得比现在更好吧:
那么,明年见吧!
2024-01-27 21:36:54
当你打开比特币(Bitcoin)网络的区块链浏览器,在少数情况下你或许能够惊讶地发现刚刚产出的一个区块是个“空区块”,就像高度为 825999 的区块一样。严格来说,这样的区块并不完全空,而是除了区块奖励交易(coinbase transaction)以外,区块内不含有任何其他交易。
理想情况下,当矿工获得一个区块的记账权后,矿工获得的收益主要来自两部分:一是区块奖励,即 coinbase 交易;另一部分则是来自在区块中包含交易所获得的手续费收益。对于这种“空区块”来讲,从经济上看起来矿工是损失了一部分收益的,但事实上并非如此……我们今天就来讨论一下这样的区块的产生原理。
比特币(Bitcoin)网络的挖矿是一种通过暴力计算符合条件的哈希值来实现工作量证明(Proof of Work,PoW)的过程,其难度如今已经变得很大。就拿上述的 825999 区块举例,该区块产出当时的全网难度约为 73.20 T。这是什么概念呢?如果一个矿工拥有一个 1Ghash/s 效率的计算设备(这比常规的家用显卡效率快得多),那么他需要约 3638791736 天(99693 个世纪!)才能成功挖掘到一个区块。这几乎是不可能独立完成的。
python3 -c "print(73.20*10**12 * 2**32 / 10**9 / 60 / 60.0 / 24)"
因此我们有了矿池。矿池按照特定的分配方式(如,按工作量以及最近收益的移动平均值)为参与计算的矿工按照所贡献的算力分配挖矿所获得的收益(与此同时,矿池自己也会获得分成)。为了计算一个区块的目标哈希值,矿工必须首先从矿池获得区块中包含的交易信息等内容后,再通过调整区块中的随机数(nonce)等值,尝试计算出符合目标要求的哈希值。区块中包含的交易数量可能很多(从区块链浏览器来看的话,一个区块中可以容纳 4,000 笔交易,有时候还要更多),因此典型上来讲,矿池可能需要几秒中的时间才能将这些编排的交易内容(矿工待解的“谜题”)整理并发送给矿工(其中还有网络传输时间以及网络延迟)。
绝大多数比特币矿工遵循最长链(Longest chain)原则工作:当矿工得知网络上有一个新的有效区块被发现时,矿工会希望基于这个最新产生的区块继续计算下一个区块的哈希。但前面我们提到,完整的下一个区块的信息可能需要几秒钟时间才能传输到矿工本地以供计算。在此期间,为了不浪费矿工宝贵的时间(以及算力),矿池通常会(预先)发送给矿工一个区块的模版信息,该信息是只包含一个区块奖励交易(coinbase transaction)的最小区块数据,以供矿工立刻开始计算。而完整的待计算区块的数据将在之后很快的时间发送给矿工。有的时候,幸运的矿工会在这(短暂的)几秒钟时间内,就基于这个最小的区块模版,计算出下一个有效的区块的结果。这个时候会发生什么?大多数矿工会选择将这个“空区块”广播到区块链网络,以获得该区块中的区块奖励。
在上面的场景中,当矿工计算出一个有效的“空区块”后,矿工虽然无法获得由于包含其他交易所带来的手续费奖励,但是矿工广播交易至少会获得确定性的由新区块带来的奖励。同样地,矿工还面临着其他矿工也计算出有效区块的压力,对手矿工可能会先于自己将另外一个有效的区块广播到网络中。因此,面对一个确定性的(但是可能较少)的奖励,以及另一个有极大不确定性的较多的奖励时,我想绝大多数矿工都会选择广播自己计算出的第一个区块的。
值得注意的是,这样的区块由于不会包含其他任何的交易信息,并且还在区块链中占用了一个区块的位置,会被一些社区成员认为是一种垃圾信息以及对比特币网络的攻击。针对这一点,有一篇发布于 LinkedIn 上的文章很好地讨论了这一点。文章中认为,尽管“空区块”不处理任何内存池中的等待交易,但它们并没有为区块链网络天价更多混乱,反而增强了所有先前区块的安全性,并重申了矿工们维护网络完整性的承诺。“空区块”实际上是对比特币巧妙激励结构的证明。在这个系统中,每个决定,即使是区块中没有交易,也都有助于网络的整体强度和韧性。空区块并不常见,通常不是攻击,也不是垃圾邮件,它们是矿工的战略决策,凸显了比特币架构中内嵌的韧性和远见。
2023-11-17 16:18:52
ELK 日志堆栈(Elasticsearch B.V. 官方称“Elastic Stack”),是由 ElasticSearch、Kibana 以及 Logstash 等组件组成的工具集,被业界广泛用于进行日志处理等任务。其中 ElasticSearch 组件本身也是一个强大不可替代的开源搜索引擎。
ElasticSearch 及其周边组件的功能非常强大,该堆栈可配置的灵活部分也很多。比如分布式集群、索引生命周期管理以及索引副本等功能。为简单起见,本文章将介绍在单台服务器配置 ELK 堆栈,以实现将 Python 应用程序的日志输出接入 ElasticSearch 的步骤。关于 ElasticSearch 的更多信息,请访问:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
Elastic 官方提供了多种在机器上安装 ELK 堆栈的方式。我们以一台安装有 Ubuntu 22.04 LTS 系统的服务器为例,演示如何通过 apt 包管理器安装 ELK 日志堆栈的方法。对于其他操作系统或安装方式,请参阅官方文档以了解更多信息。
今天我们的示例将会安装 ELK 堆栈的全部组件:ElasticSearch,本身是一个性能强大且易于拓展的数据和搜索引擎;Kibana,可视化数据并提供图形界面的组件;以及 Logstash,从任何方式收集日志,并输出至任意位置的可配置的数据流管线;这些组件将驱动整个 Python 应用程序的日志收集与处理流程。
ELK 堆栈主要由 Java 编写,因此第一步骤是要在系统中安装 Java 运行时环境(Java Runtime Environment,JRE)。我们以开源的 OpenJDK 为例,使用以下命令进行安装:
sudo apt install default-jre
在 Ubuntu 22.04 LTS 系统中,这将会安装 openjdk-11-jre
包。
ElasticSearch 通过 apt 包管理器的安装非常简单,使用以下命令进行安装:
sudo apt install elasticsearch
安装完成后,注意安装脚本给出的超级用户的密码,以及最后一部分提示:
Reset the password of the elastic built-in superuser with
'/usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic'.
Generate an enrollment token for Kibana instances with
'/usr/share/elasticsearch/bin/elasticsearch-create-enrollment-token -s kibana'.
Generate an enrollment token for Elasticsearch nodes with
'/usr/share/elasticsearch/bin/elasticsearch-create-enrollment-token -s node'.
ElasticSearch 默认的超级用户的用户名是 elastic
,密码是安装日志上显示的密码。如果忘记,可以通过执行上方日志的命令将其重置。
ElasticSearch 安装完毕后,使用 root 权限编辑 /etc/elasticsearch/elasticsearch.yml
文件,并修改或添加以下配置:
network.host: localhost
http.host: localhost
使用以下命令在服务器上安装 Kibana:
sudo apt install kibana
Kibana 安装好后,在服务器本地浏览器访问 http://localhost:5601/
,以执行 Kibana 的初始化配置。配置过程中,可能需要填写 ElasticSearch 的配对令牌(enrollment token),该令牌的内容可以通过执行上一小节安装 ElasticSearch 后打印的提示命令获得。
如果用户是通过 SSH 连接到远程服务器进行配置的,可以考虑在 SSH 连接参数后追加
-L 5601:localhost:5601
,通过 SSH 端口转发,从而在本地客户端浏览器中访问localhost:5601
以达到相同效果
通过超级用户 elastic
以及对应密码登录 Kibana 面板后,我们鼓励新增两个单独的用户分别用作日志管理与日志推送,以实现权限控制。登录 Kibana 面板后,点击左侧的“Management” > “Stack Management”,然后首先新建用户的权限。我们以两个用户名称为 user
和 python-logger
为例:
权限名称 | 集群权限 | 索引权限 |
---|---|---|
user |
kibana_admin |
对于 * (全部索引),拥有 all 权限 |
python-logger |
monitor management
|
对于 * (全部索引),拥有 all 权限 |
添加完用户权限后,我们实际添加名为 user
和 python-logger
的用户。添加过程中,要记得赋予用户同名的用户权限。事实上,这些用户名和权限名称都是可以自定义的,这里仅为示例。
添加完成后,编辑 /etc/kibana/kibana.yml
文件,添加或修改以下配置项:
elasticsearch.hosts: ['https://localhost:9200']
如果你不希望每次登录 Kibana 面板时都输入用户名密码,可以在 /etc/kibana/kibana.yml
文件中追加以下配置项。注意,务必锁好 Kibana 面板的访问控制,否则任意用户都可以看到你的日志(并有权限篡改它们!):
xpack.security.authc.providers:
anonymous.anonymous1:
order: 0
credentials:
username: "你的用户名"
password: "你的密码"
Logstash 是控制日志从哪里收集、输出到哪里的一个“管道”管理器。我们通过以下命令安装 Logstash:
sudo apt install logstash
Logstash 的配置位于 /etc/logstash/conf.d/
目录中,如果该目录中没有文件,则新建任何后缀名为 .conf 的文件,均可以添加一个“日志管道”。作为示例,我们在该目录中创建一个名为 01-python.conf
的文件,具体内容如下:
input {
tcp {
port => 12345
codec => json
tags => "python-service"
}
}
output {
if "python-service" in [tags] {
elasticsearch {
index => "python-service-%{+YYYY-MM-dd}"
ssl_enabled => true
ssl_verification_mode => none
user => "python-logger"
password => "对应密码"
}
}
}
在上述文件中,我们定义了一个从 TCP 12345 端口接受 JSON 格式的日志输入,将其输出到本地 ElasticSearch 的日志管线。在 ElasticSearch 中,每个日志都需要在一个索引(Index)中。业界惯常的索引命名习惯是按天分割索引文件,也就是上边的 index => "python-service-%{+YYYY-MM-dd}"
一行。这将创建名称例如 python-service-2023-12-31
、python-service-2024-01-01
这样的索引。
添加好 Logstash 的配置文件后,我们通过执行以下命令应用更改:
sudo systemctl restart logstash
可以通过以下命令查看 Logstash 本身的运行日志,以诊断任何报错:
sudo journalctl -u logstash
理论上重新启动 Logstash 后,我们可以通过 sudo netstat -unltp
注意到 Logstash 已经在监听 TCP 12345 端口。我们可以使用下列示例 Python 脚本向 Logstash 发送内容,并登录 Kibana 进行查看:
import logging
import logstash # 需要 pip install python-logstash
import sys
host = 'localhost'
test_logger = logging.getLogger('python-logstash-logger')
test_logger.setLevel(logging.INFO)
test_logger.addHandler(logstash.TCPLogstashHandler(host, 12345, version=1))
test_logger.error('Hello, world! This is an error')
test_logger.info('Hello, world! This is an info')
test_logger.warning('Hello, world! This is a warning')
同样地,请为服务器端的 TCP 12345 端口(或者其他你喜欢的端口数字)设置恰当的访问权限控制,以避免互联网上的任意用户向日志索引中追加内容。
Logstash 本身的配置文件功能很多,可以从多种数据源及不同数据格式中接收日志,并输出到指定位置(不仅包括 ElasticSearch)。关于 Logstash 配置文档的更多信息,请查看:https://www.elastic.co/guide/en/logstash/current/configuration-file-structure.html
到目前为止,ELK 堆栈的全部内容已经配置完成了。现在我们开始配置 Python 应用程序。如果你使用的是 Django,则需要在 LOGGING 配置项中进行类似这样的配置:
LOGGING = {
...
'handlers': {
'logstash': {
'level': 'DEBUG',
'class': 'logstash.LogstashHandler', # 如果日志格式有问题,可以改用 TCPLogstashHandler
'host': 'localhost',
'port': 12345,
'version': 1, # Version of logstash event schema. Default value: 0 (for backward compatibility of the library)
'message_type': 'logstash', # 'type' field in logstash message. Default value: 'logstash'.
'fqdn': False, # Fully qualified domain name. Default value: false.
'tags': ['tag1', 'tag2'], # list of tags. Default: None.
},
},
'loggers': {
'django.request': {
'handlers': ['logstash'],
'level': 'DEBUG',
'propagate': True,
},
},
...
}
如果你使用其他框架或直接调用 logger 进行日志输出,配置方式可能像这样:
import logging
import logstash
import sys
host = 'localhost'
test_logger = logging.getLogger('python-logstash-logger')
test_logger.setLevel(logging.INFO)
test_logger.addHandler(logstash.LogstashHandler(host, 12345, version=1))
默认情况下 ElasticSearch 不会删除任何日志信息。这可能会导致日志量增大时,服务器磁盘空间不足。这里给出了一个我使用 Python 编写的自动清除 xx 天前索引的脚本,可以通过定时运行(如 crontab)脚本的方式,实现日志的按天滚动清除:
from datetime import datetime, timedelta
from typing import List
from dataclasses import dataclass
import requests
from requests.auth import HTTPBasicAuth
@dataclass(frozen=True)
class IndexConfig(object):
delete_threshold: timedelta
delete_no_suffix: bool = False
ELASTIC_USERNAME = "管理用户的用户名"
ELASTIC_PASSWORD = "对应密码"
ELASTIC_AUTH = HTTPBasicAuth(username=ELASTIC_USERNAME, password=ELASTIC_PASSWORD)
ELASTIC_API_BASEURL = "https://localhost:9200"
ELASTIC_INDEX_PREFIXES_CONFIG = {
"python-service": IndexConfig(delete_threshold=timedelta(days=7)) # 在这里设置要自动删除的索引名称,以及间隔几天自动清除旧索引
}
def get_all_indexes() -> List[str]:
return requests.get(f"{ELASTIC_API_BASEURL}/{index_prefix}*", auth=ELASTIC_AUTH, verify=False).json()
def delete_index(index_name: str):
print(f"Delete index: {index_name}")
return requests.delete(f"{ELASTIC_API_BASEURL}/{index_name}", auth=ELASTIC_AUTH, verify=False).json()
for index_prefix, index_config in ELASTIC_INDEX_PREFIXES_CONFIG.items():
for index in get_all_indexes():
index_suffix = index[len(index_prefix)+1:]
if not index_suffix:
if index_config.delete_no_suffix:
delete_index(index)
continue
try:
index_datetime = datetime.fromisoformat(index_suffix)
except ValueError:
print(f"Warning: index_datetime invalid parsing: {index_suffix}.")
continue
if datetime.now() - index_datetime >= index_config.delete_threshold:
delete_index(index)
上述脚本需要 Python 的第三方库 requests。
Kibana 可以配置更多的登陆方式,比如使用 Google 单点登录(Google SSO),但这些功能属于 ELK 的高级订阅层级支持的功能。如果要了解更多信息,可以参考 https://www.elastic.co/guide/en/cloud/current/ec-securing-clusters-oidc-op.html。