[{"content":"Candidate Materials — Dongpo Liu Overview Thank you for taking the time to review my materials.\nThis document collects several samples of my work, writing, analysis, and presentations. For each sample, I have included a short description of the context and why I selected it.\n1. Work Samples Work Sample 1: Auto Analyze Priority Queue Link: Auto Analyze priority queue implementation\nContext: I implemented a priority-based scheduling mechanism for automatic table statistics collection in the database background worker system. The scheduler was designed to keep table statistics fresh while avoiding starvation and preventing small, frequently updated tables from dominating the queue.\nThis work was important because statistics are a critical input to the query optimizer. Accurate and timely statistics improve cardinality estimation, cost estimation, and the overall quality of query plan selection.\nWhy I selected this sample: This is one of the most important projects I have delivered at PingCAP. I have been iterating on this module for almost three years, improving its correctness, reliability, and maintainability over time as production requirements evolved.\nOutcome:\nImproved the fairness and efficiency of auto-analyze scheduling, significantly reducing table starvation. Eliminated the overhead of repeatedly rebuilding the priority queue by keeping it in memory, reducing CPU usage by approximately one core and lowering memory consumption by 99%. Notes:\nThe writing samples provide additional design context for this implementation.\nWork Sample 2: cargo info Command Link: cargo info pull request\nContext: This project closed a 10-year-old Cargo issue by introducing a new subcommand, cargo info, which allows users to inspect Rust package information directly from Cargo.\nI designed and developed the feature together with Ed. The project reflects my deep involvement in Cargo: after contributing to Cargo for about five years, I was invited to join the Cargo team, and I have now been contributing to the project for around seven years.\nWhy I selected this sample: I selected this sample because it is one of the largest and most important features I have delivered in Cargo. The work happened in a fully open-source environment, where Cargo has high standards for code quality, documentation, review, and collaboration.\nThis project demonstrates my ability to work effectively outside my usual professional environment and to deliver a high-quality feature in a mature and widely used open-source project.\nImpact: The feature has since been merged into Cargo as an official subcommand, where it is used by developers as part of their everyday workflow.\n2. Writing Samples Writing Sample 1: RFC: Priority Queue for Auto Analyze Link: Priority Queue for Auto Analyze RFC\nContext: This RFC proposed introducing a priority queue for automatic statistics collection in TiDB. It was my first RFC after joining the optimizer team.\nThe design went through a long iteration process, including multiple rounds of internal discussion, before we converged on the final approach. For the corresponding implementation, please refer to Work Sample 1.\nWhy I selected this sample: I selected this sample because it was my first RFC after joining the optimizer team. It required me to learn a new codebase, understand the existing design constraints, and collaborate closely with new teammates.\nThe RFC went through many rounds of discussion and iteration. I used that process not only to improve the design, but also to build trust and working relationships with the team.\nWriting Sample 2: RFC: Maintain The Priority Queue In Memory Link: Maintain the Priority Queue in Memory RFC\nContext: This RFC was a follow-up iteration on Writing Sample 1. After the initial priority queue design was implemented, we encountered new customer requirements that made it necessary to avoid rebuilding the queue repeatedly. Re-scanning millions of table objects over and over again was too expensive.\nThe main challenge was maintaining the priority queue in memory while handling DDL events correctly. I worked with a colleague from the DDL team to design a solution that kept the in-memory queue consistent as table metadata changed.\nWhy I selected this sample: I selected this sample because it shows how the original design evolved over time. Writing Sample 1 left one important problem unresolved: how to maintain the priority queue in memory to avoid repeatedly rescanning all tables and rebuilding the queue. About a year later, we revisited that problem and delivered a follow-up design.\nWhat I like about this work is that it did not require breaking the previous design. Instead, it demonstrated that the original design was extensible enough to support the new requirement. The hardest part was reasoning clearly about DDL ordering and keeping the in-memory queue consistent as table metadata changed.\nThis work also required close collaboration with the DDL team, making it a good example of cross-team design and delivery.\n3. Analysis Samples Analysis Sample 1: Memory Leak Link: TiDB memory leak RCA\nContext: I investigated a customer-reported TiDB memory leak in the asynchronous statistics-loading path, reproduced the issue by forcing histogram-loading failures, and helped narrow it down to resource cleanup problems in the error-handling path.\nProblem statement: In the customer\u0026rsquo;s production environment, TiDB\u0026rsquo;s memory usage kept increasing. However, we did not have access to the customer\u0026rsquo;s actual workload. This made it difficult to identify the root cause from the available observability data.\nApproach: Because the available observability data did not provide enough clues, I investigated the issue by tracing the relevant code path line by line. Although I was not very familiar with this part of the codebase at the time, I used the limited signals we had: the customer had changed the tidb_opt_objective parameter, and there were error logs related to statistics loading.\nI started from the code path where tidb_opt_objective is set, then followed how it affects optimization behavior and statistics loading. This eventually allowed me to reproduce the same error logs and memory growth locally.\nWhy I selected this sample: I selected this sample because it reflects a debugging approach I value highly: carefully reading the code until the system behavior becomes understandable. This investigation happened before AI-assisted coding tools were part of my regular workflow, so I had to trace the relevant paths manually, line by line.\nThat process eventually revealed the clues needed to reproduce the issue. It reinforced my belief that, for difficult system problems, the source code is often the most reliable source of truth.\nAnalysis Sample 2: AI-Assisted RCA for a Seven-Year-Old Bug Link: Seven-year-old statistics update bug RCA\nContext: TiDB’s statistics modify_count could fail to update after DML; even after inserting a new row into an analyzed table, SHOW STATS_META could still report Modify_count = 0, which could make table-change tracking inaccurate and affect auto-analyze decisions.\nProblem statement: In the customer\u0026rsquo;s production environment, a table received a large number of updates, but TiDB failed to record those changes in its internal statistics metadata. Since this metadata is used to decide when table statistics should be refreshed, the table could incorrectly appear unchanged.\nAlthough TiDB periodically flushes table modification information every five minutes, the update count was never persisted in this case.\nApproach: We encountered this issue at least twice. The first time, we were not able to identify the root cause. The second time, we still struggled at first, partly because we assumed that the five-minute forced flush mechanism was reliable: it had been in the codebase for about seven years, so we naturally believed it was working correctly.\nBecause of that assumption, I spent a lot of time reading other parts of the code while initially overlooking this old mechanism. After I still could not find the problematic code path, I went back to collect more information and used AI to help me inspect the code more systematically.\nIt turned out that the code path I had assumed to be correct had actually never worked properly for the past seven years. Once I realized that this mechanism was not trustworthy, I was able to reproduce the issue, identify the root cause, and fix it.\nWhy I selected this sample: I selected this sample because it shows another way I approach root-cause analysis. My first instinct was to read the code carefully myself, but in this case, AI tools turned out to be extremely useful for exploring the code quickly and without the assumptions I had developed from working on this system every day.\nThis was one of the first moments where I felt that collaborating with AI materially improved my debugging process. It was also the first real on-call issue I solved with significant help from AI-assisted code exploration.\n4. Presentation Presentation Sample 1: Shipping TiKV with Cargo Link: Recording: Shipping TiKV with Cargo recording\nSlides: Shipping TiKV with Cargo slides\nContext: This was a presentation I gave at Rust Munich 2026. In the talk, I shared practical knowledge about Cargo and explained how we use Cargo to manage and ship a complex production project.\nWhy I selected this sample: I selected this sample because it was my first in-person technical talk in English. Although I had given many internal and online technical presentations before, delivering a talk in person was a new challenge for me.\nIt was personally meaningful, and it also gave me an opportunity, as a Cargo maintainer, to share practical knowledge and real-world lessons with the Rust community.\nPresentation Sample 2: TiDB Analyze Link: Recording: TiDB Analyze recording\nSlides: TiDB Analyze slides\nContext: This was a presentation I first gave internally and later shared publicly. It explains TiDB’s ANALYZE feature and walks through the full workflow of the module.\nI prepared this talk about one year after joining the team. At the time, I wanted to build a deeper understanding of the system, so preparing the presentation was also a learning process for me. I used demos and animations to make the internal workflow easier to understand and to explain how each component fits together.\nWhy I selected this sample: I selected this sample because it reflects how I often learn: by teaching. Preparing a talk forces me to understand a system deeply enough to explain it clearly to others.\nThis presentation was also a summary of my first year working on TiDB statistics. During that time, I tried to build a deep understanding of the system, continuously identify issues, and develop a clearer sense of what mattered most and where the system needed to improve.\n5. Summary The materials above are intended to show how I approach engineering work: understanding complex systems, communicating trade-offs, debugging production issues, writing maintainable software, and collaborating with others.\nThank you again for reviewing them.\n","permalink":"https://0xpoe.dev/posts/2026-06-29-my-work/","summary":"\u003ch1 id=\"candidate-materials--dongpo-liu\"\u003eCandidate Materials — Dongpo Liu\u003c/h1\u003e\n\u003ch2 id=\"overview\"\u003eOverview\u003c/h2\u003e\n\u003cp\u003eThank you for taking the time to review my materials.\u003c/p\u003e\n\u003cp\u003eThis document collects several samples of my work, writing, analysis, and presentations. For each sample, I have included a short description of the context and why I selected it.\u003c/p\u003e\n\u003ch2 id=\"1-work-samples\"\u003e1. Work Samples\u003c/h2\u003e\n\u003ch3 id=\"work-sample-1-auto-analyze-priority-queue\"\u003eWork Sample 1: Auto Analyze Priority Queue\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eLink:\u003c/strong\u003e \u003ca href=\"https://github.com/pingcap/tidb/blob/master/pkg/statistics/handle/autoanalyze/refresher/refresher.go\"\u003eAuto Analyze priority queue implementation\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eContext:\u003c/strong\u003e I implemented a priority-based scheduling mechanism for automatic table statistics collection in the database background worker system. The scheduler was designed to keep table statistics fresh while avoiding starvation and preventing small, frequently updated tables from dominating the queue.\u003c/p\u003e","title":"Candidate Materials — Dongpo Liu"},{"content":" ","permalink":"https://0xpoe.dev/presentations/2026-02-04-ship-with-cargo/","summary":"\u003ciframe src=\"https://ship-with-cargo.slides.0xpoe.dev/\" width=\"100%\" height=\"500px\" frameborder=\"0\" allowfullscreen\u003e\u003c/iframe\u003e","title":"Shiping TiKV with Cargo"},{"content":"Statistics Statistics collection is a crucial process of modern database systems, forming the backbone of query optimization. In TiDB, statistics are indispensable, serving as the sole source of information for estimating query costs and selecting the most efficient execution plan.\nTiDB collects several types of statistics for each table, including:\nTopN values (most frequent values to reflect data skewness) Histograms (data distribution) Number of Distinct Values (NDV) Other statistical metrics These statistics will be stored in some system tables, such as mysql.stats_meta, mysql.stats_top_n, mysql.stats_histograms, and mysql.stats_buckets.\nWhen TiDB starts, it must load these statistics from the system tables into memory, a process known as \u0026ldquo;statistics initialization.\u0026rdquo; This step is crucial as it equips the query optimizer with the necessary statistical information to generate optimal execution plans.\nIn this blog post, we will discuss the initialization process of TiDB statistics.\nInitialization Process What Statistics Data Gets Loaded? In TiDB, the statistics data can be divided into three parts:\nNote: If you are not familiar with these statistics fields yet, do not worry. We will explain them in the following sections.\nBasic information: modify_count, count, version from mysql.stats_meta table. TopN information: value, count from mysql.stats_topn table. Histogram meta: is_index, distinct_count, null_count, version, stats_ver from mysql.stats_histograms table. Histogram buckets: count, repeats, lower_bound, upper_bound from mysql.stats_buckets table. When TiDB starts, it will load these statistics data from the system tables into memory.\nHow TiDB Loads Statistics Data? Since TiDB maintains extensive statistics for each column and index (including 100 TopN values and 256 histogram buckets), loading all statistics at once can be resource-intensive. To address this, TiDB provides two initialization approaches:\nA lightweight mode that quickly loads only essential statistics A comprehensive mode that loads all statistical data but takes longer to complete This dual-mode approach allows TiDB to balance between startup speed and statistical completeness based on specific needs. But it also brings extra complexity and maintenance burden.\nLightweight Mode In lightweight mode, TiDB loads only essential statistical information from two system tables:\nFrom mysql.stats_meta:\nmodify_count: Number of row modifications since the last statistics collection count: Total number of rows version: Last update time of the stats meta table (This is the update time of the stats meta row, not the last analysis time). From mysql.stats_histograms:\nis_index: Whether the statistics are for an index. distinct_count: Number of distinct values. null_count: Number of NULL values. version: Last update time of the statistics.(The real last analysis time) stats_ver: Statistics format version(0, 1, 2). 0 means the statistics have not been analyzed yet. The purpose of loading this basic information is to provide the modify_count and count metrics to other modules, allowing them to track both the real-time total number of rows in the table and how many rows have been modified since the last statistics collection.\nComprehensive Mode In comprehensive mode, TiDB loads all statistical data from the system tables. This includes:\nFrom mysql.stats_meta:\nmodify_count: Number of row modifications since the last statistics collection count: Total number of rows version: Last update time of the stats meta table (This is the update time of the stats meta row, not the last analysis time). From mysql.stats_histograms:\nis_index: Whether the statistics are for an index. distinct_count: Number of distinct values. null_count: Number of NULL values. version: Last update time of the statistics.(The real last analysis time) stats_ver: Statistics format version(0, 1, 2). 0 means the statistics have not been analyzed yet. From mysql.stats_topn: (only for indexes)\nvalue: TopN value. (With data type) count: Count of the TopN value. From mysql.stats_buckets: (only for indexes)\ncount: Number of rows in the bucket. repeats: Number of times the upper-bound value of the bucket appears in the data. lower_bound: Lower bound of the bucket. upper_bound: Upper bound of the bucket. However, it is important to note that the comprehensive mode only loads statistics for indexes, not for columns. For columns, TiDB will load the statistics on-demand when the column is accessed for the first time.\nConfiguration Because TiDB has two initialization modes, it provides these configuration options to specify which mode to use:\nlite-init-stats: Chooses the lightweight statistics initialization mode, defaulting to true after v7.2.0. force-init-stats: Forces TiDB to start before fully loading statistics, defaulting to true after v7.2.0. Enabling this with the comprehensive mode lengthens startup times. Disabling it may cause TiDB to serve queries with incomplete statistics. concurrently-init-stats: Enables concurrent statistics initialization, defaulting to true after v8.1.0. This option can speed up the initialization process by loading statistics concurrently. Since the lightweight mode is already sufficiently fast, this setting is most useful for the comprehensive mode. Since these configurations are set in the configuration file and not as system variables, you need to restart the TiDB server to apply the changes. Therefore, ensure you adjust these settings according to your needs before starting the TiDB server.\nMaintain Statistics In Memory The initialization process appears straightforward: load statistics data from system tables into memory. However, maintaining these statistics in memory is a complex task that involves several challenges:\nFind a suitable data structure to store statistics data. Manage the memory usage of statistics data to avoid excessive consumption. Data Structure TiDB uses a hierarchical data structure to store statistics data in memory. The structure is as follows:\ntype Table struct { ... ColAndIdxExistenceMap *ColAndIdxExistenceMap HistColl Version uint64 LastAnalyzeVersion uint64 ... } ColAndIdxExistenceMap: A map that indicates which columns and indexes have real statistics data to avoid unnecessary statistics loading. We will discuss this in my next blog post. HistColl: A collection of histograms for each column or index. (This is the biggest structure that contains all the statistical data.) version: Last update time of the stats meta table (This is the update time of the stats meta row, not the last analysis time). LastAnalyzeVersion: The last analysis time of the statistics. The ColAndIdxExistenceMap used to track which columns and indexes have real statistics data. It is a simple map that stores the column or index ID as the key and a boolean value as the value to indicate whether the column or index has been analyzed.\ntype ColAndIdxExistenceMap struct { checked bool colAnalyzed map[int64]bool idxAnalyzed map[int64]bool } func (m *ColAndIdxExistenceMap) HasAnalyzed(id int64, isIndex bool) bool { if isIndex { analyzed, ok := m.idxAnalyzed[id] return ok \u0026amp;\u0026amp; analyzed } analyzed, ok := m.colAnalyzed[id] return ok \u0026amp;\u0026amp; analyzed } Whenever you need to determine if an index or column has actual statistics data, you can use the HasAnalyzed method to check this map.\nNote: We could optimize memory usage by implementing a bitmap.\nThe HistColl structure serves as the central data structure for storing statistical information.\ntype HistColl struct { ... columns map[int64]*Column indices map[int64]*Index PhysicalID int64 RealtimeCount int64 ModifyCount int64 StatsVer int Pseudo bool ... } It maintains two maps:\ncolumns: Maps column IDs to column statistics indices: Maps index IDs to index statistics These maps enable quick lookups of statistics by ID. It also contains other table-level statistics, such as RealtimeCount, ModifyCount, StatsVer, and Pseudo.\nFor columns, each entry contains extensive statistical information:\ntype Column struct { LastAnalyzePos types.Datum ... TopN *TopN ... Info *model.ColumnInfo Histogram StatsLoadedStatus PhysicalID int64 StatsVer int64 IsHandle bool } For indexes, the structure is similar:\ntype Index struct { LastAnalyzePos types.Datum ... TopN *TopN ... Info *model.IndexInfo Histogram StatsLoadedStatus StatsVer int64 PhysicalID int64 } As you can see, the main statistical data is stored in the Histogram structure, which contains multiple buckets to represent the data distribution.\ntype Bucket struct { Count int64 Repeat int64 NDV int64 } type Histogram struct { Tp *types.FieldType Bounds *chunk.Chunk Buckets []Bucket Scalars []scalar ID int64 NDV int64 NullCount int64 LastUpdateVersion uint64 Correlation float64 } The data structure used to store statistics in memory is complex and hierarchical, which can make it difficult to understand and maintain.\nTo summarize, we can use this diagram to illustrate the hierarchical data structure used to store statistics in memory:\nLRU Stats Cache Ideally, TiDB would load all statistics at startup, but this is impractical given the large volumes of data. To keep memory usage in check, TiDB relies on an LRU cache that loads and evicts statistics on demand.\nThe interface of the LRU cache is as follows:\ntype StatsCacheInner interface { Get(tid int64) (*statistics.Table, bool) Put(tid int64, tbl *statistics.Table) bool Del(int64) Cost() int64 Values() []*statistics.Table Len() int Copy() StatsCacheInner SetCapacity(int64) Close() TriggerEvict() } This interface implemented by the LRU structure:\ntype LFU struct { cache *ristretto.Cache resultKeySet *keySetShard cost atomic.Int64 closed atomic.Bool closeOnce sync.Once } This LFU structure is a wrapper of the ristretto.Cache structure, which is a high-performance cache library in Go. It provides a simple API to manage the cache and handle the eviction of statistics data.\nTo initialize the ristretto.Cache, we need to specify the maximum cost to control memory usage. Additionally, we need to implement three hooks to handle the eviction of statistics data. Each time we put, get, or delete data from the cache, the corresponding hook will be triggered.\n// NewLFU creates a new LFU cache. func NewLFU(totalMemCost int64) (*LFU, error) { cost, err := adjustMemCost(totalMemCost) if err != nil { return nil, err } ... result := \u0026amp;LFU{} bufferItems := int64(64) cache, err := ristretto.NewCache( \u0026amp;ristretto.Config{ NumCounters: max(min(cost/128, 1_000_000), 10), MaxCost: cost, BufferItems: bufferItems, OnEvict: result.onEvict, OnExit: result.onExit, OnReject: result.onReject, ... }, ) if err != nil { return nil, err } result.cache = cache result.resultKeySet = newKeySetShard() return result, err } There are three hooks to handle the eviction of statistics data:\nOnEvict: Called for every eviction and passes the hashed key, value, and cost to the function. OnExit: Called whenever a value is removed from cache. OnReject: Called for every rejection done via the policy. Whenever table statistics are evicted from the cache, TiDB prunes the data from its in-memory structures. The OnEvict hook triggers the dropMemory function, which finalizes the eviction and updates the memory usage to reflect the change.\nfunc (s *LFU) dropMemory(item *ristretto.Item) { ... table := item.Value.(*statistics.Table).Copy() table.DropEvicted() s.resultKeySet.AddKeyValue(int64(item.Key), table) after := table.MemoryUsage().TotalTrackingMemUsage() s.addCost(after) s.triggerEvict() } In the dropMemory function, we first make a copy of the table statistics and then call the DropEvicted method to prune the data. This method will drop the TopN and Histogram data and mark the column or index as AllEvicted. Basically, it reinitializes the statistics data to free up memory.\nfunc (coll *HistColl) DropEvicted() { for _, col := range coll.columns { if !col.IsStatsInitialized() || col.GetEvictedStatus() == AllEvicted { continue } col.DropUnnecessaryData() } for _, idx := range coll.indices { if !idx.IsStatsInitialized() || idx.GetEvictedStatus() == AllEvicted { continue } idx.DropUnnecessaryData() } } func (c *Column) DropUnnecessaryData() { if c.StatsVer \u0026lt; Version2 { c.CMSketch = nil } c.TopN = nil c.Histogram.Bounds = chunk.NewChunkWithCapacity([]*types.FieldType{types.NewFieldType(mysql.TypeBlob)}, 0) c.Histogram.Buckets = make([]Bucket, 0) c.Histogram.Scalars = make([]scalar, 0) c.evictedStatus = AllEvicted } You might notice that the dropMemory method includes the newly updated table statistics cost in the cache. Consequently, when the memory usage is cleared, the OnExit hook is triggered to reflect this change in memory usage.\nfunc (s *LFU) onExit(val any) { ... s.addCost(-val.(*statistics.Table).MemoryUsage().TotalTrackingMemUsage()) } The OnReject hook behaves similarly to the OnEvict hook. When triggered, it calls the dropMemory function to finalize the eviction and adjust memory usage accordingly.\nfunc (s *LFU) onReject(item *ristretto.Item) { ... s.dropMemory(item) } When the dropMemory function is called, it removes large data structures but also stores basic table statistics in the resultKeySet cache. This preserves essential details like modify_count, count, and version for quick retrieval. TiDB uses two caching layers for statistics: the LRU cache for frequently accessed tables with full stats and the resultKeySet cache, which is a simple map of table IDs to essential table information. The Put, Get, and Del methods in the LFU structure illustrate how these two layers interact.\nfunc (s *LFU) Put(tblID int64, tbl *statistics.Table) bool { cost := tbl.MemoryUsage().TotalTrackingMemUsage() s.resultKeySet.AddKeyValue(tblID, tbl) s.addCost(cost) return s.cache.Set(tblID, tbl, cost) } func (s *LFU) Get(tid int64) (*statistics.Table, bool) { result, ok := s.cache.Get(tid) if !ok { return s.resultKeySet.Get(tid) } return result.(*statistics.Table), ok } func (s *LFU) Del(tblID int64) { s.cache.Del(tblID) s.resultKeySet.Remove(tblID) } The Put method adds the table statistics to the LRU cache and the resultKeySet cache. The Get method retrieves the table statistics from the LRU cache and falls back to the resultKeySet cache if the data is not found. The Del method removes the table statistics from both caches.\nConfiguration To configure the LRU cache, you can set the following system variable:\ntidb_stats_cache_mem_quota: Specifies the maximum memory usage for the statistics cache. The default value is half of the total memory of the TiDB instance. Recently, the default value has been changed to 20% of the total memory. Determining the optimal value for this variable can be challenging as it depends on the workload and the size of the statistics data. I will revisit this topic in a future blog post after we have more insights into the on-fly statistics loading feature.\nConclusion In this blog post, we covered TiDB’s statistics initialization process and described how TiDB loads data from system tables. We also detailed TiDB’s dual-mode initialization approach and the data structures used to store statistics. Additionally, we examined how TiDB maintains these statistics in memory using an LRU cache to balance performance and resource usage. Future posts will address column-level statistics loading on demand and how TiDB updates its statistics.\n","permalink":"https://0xpoe.dev/posts/2025-02-05-init-stats/","summary":"\u003ch1 id=\"statistics\"\u003eStatistics\u003c/h1\u003e\n\u003cp\u003eStatistics collection is a crucial process of modern database systems, forming the backbone of query optimization. In TiDB, statistics are indispensable, serving as the sole source of information for estimating query costs and selecting the most efficient execution plan.\u003c/p\u003e\n\u003cp\u003eTiDB collects several types of statistics for each table, including:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eTopN values (most frequent values to reflect data skewness)\u003c/li\u003e\n\u003cli\u003eHistograms (data distribution)\u003c/li\u003e\n\u003cli\u003eNumber of Distinct Values (NDV)\u003c/li\u003e\n\u003cli\u003eOther statistical metrics\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThese statistics will be stored in some system tables, such as \u003ccode\u003emysql.stats_meta\u003c/code\u003e, \u003ccode\u003emysql.stats_top_n\u003c/code\u003e, \u003ccode\u003emysql.stats_histograms\u003c/code\u003e, and \u003ccode\u003emysql.stats_buckets\u003c/code\u003e.\u003c/p\u003e","title":"TiDB Statistics: Understanding the Initialization Process"},{"content":"std::cmp The std::cmp module offers several traits for comparing values. It allows us to check for equality and determine the ordering of values. Unlike some other languages where the compiler automatically provides these implementations, Rust requires us to explicitly implement or derive these traits.\nImplementing these traits can sometimes be challenging. In this post, we will explore the PartialEq, Eq, PartialOrd, Ord, and Reverse traits in detail.\nPartialEq Many might assume that PartialEq is termed \u0026ldquo;partial\u0026rdquo; because it only compares certain parts of a type. However, the \u0026ldquo;partial\u0026rdquo; in PartialEq actually indicates that the equality comparison does not need to be reflexive (a == a). In other words, PartialEq does not require a type to always be equal to itself.\nThe PartialEq trait is defined as follows:\npub trait PartialEq\u0026lt;Rhs = Self\u0026gt; where Rhs: ?Sized, { // Required method fn eq(\u0026amp;self, other: \u0026amp;Rhs) -\u0026gt; bool; // Provided method fn ne(\u0026amp;self, other: \u0026amp;Rhs) -\u0026gt; bool { ... } } The PartialEq trait requires the implementation of the eq method, which checks if two values are equal. By default, the ne method is provided, which checks if two values are not equal. Essentially, eq corresponds to the == operator, while ne corresponds to the != operator.\nAt first glance, it might seem that the PartialEq trait is sufficient for determining equality between values. However, from a mathematical standpoint, equality should be reflexive, meaning a value must always be equal to itself.\nIt may seem intuitive that all types should exhibit reflexive equality, there are exceptions. For instance, the floating-point types f32 and f64 include a special value called NaN (Not a Number). NaN is unique in that it is not equal to itself, thus violating the reflexivity property. Consequently, PartialEq does not enforce reflexivity by default.\nAccording to the IEEE 754 standard, comparisons involving NaN always return false, even when comparing NaN with itself. This means that if x is NaN, the following comparisons will all return false:\nx == x x \u0026lt; x x \u0026gt; x However, the comparison x != x will return true.\nSo in some of data structures, like HashSet and HashMap, the PartialEq trait is not sufficient for some of their operations. For example, the contains_key method of HashMap requires the Eq trait, which enforces reflexivity.\npub fn contains_key\u0026lt;Q\u0026gt;(\u0026amp;self, k: \u0026amp;Q) -\u0026gt; bool where K: Borrow\u0026lt;Q\u0026gt;, Q: Hash + Eq + ?Sized, As a result, it is impossible to create a HashMap with f32 or f64 keys because they do not implement the Eq trait.\nlet mut map: HashMap\u0026lt;f64, String\u0026gt; = HashMap::new(); map.insert(3.14, String::from(\u0026#34;Pi\u0026#34;)); map.insert(2.71, String::from(\u0026#34;Euler\u0026#34;)); Attempting to compile this code will result in an error because f64 does not satisfy the Eq trait requirements.\nerror[E0599]: the method `insert` exists for struct `HashMap\u0026lt;f64, String\u0026gt;`, but its trait bounds were not satisfied --\u0026gt; src/main.rs:8:9 | 8 | map.insert(3.14, String::from(\u0026#34;Pi\u0026#34;)); | ^^^^^^ | = note: the following trait bounds were not satisfied: `f64: Eq` `f64: Hash` So Rust provides the Eq trait, which extends the PartialEq trait by enforcing reflexivity. We will discuss the Eq trait in more detail later in this post.\nTo implement the PartialEq trait, you can either do it manually or use the derive attribute.\nFor example, you can derive the PartialEq trait for a custom type like this:\n#[derive(PartialEq)] struct Point { x: i32, y: i32, } Expanding the derive attribute generates the following implementation:\nstruct Point { x: i32, y: i32, } #[automatically_derived] impl ::core::marker::StructuralPartialEq for Point {} #[automatically_derived] impl ::core::cmp::PartialEq for Point { #[inline] fn eq(\u0026amp;self, other: \u0026amp;Point) -\u0026gt; bool { self.x == other.x \u0026amp;\u0026amp; self.y == other.y } } As shown, the PartialEq trait is implemented for the Point struct. The eq method checks if the x and y fields of two Point instances are equal.\nFor enums, the derive attribute generates an implementation that checks if the variants are equal:\n#[derive(PartialEq)] enum Direction { Up, Down, Left, Right, } The derive attribute generates the following implementation:\nenum Direction { Up, Down, Left, Right, } #[automatically_derived] impl ::core::marker::StructuralPartialEq for Direction {} #[automatically_derived] impl ::core::cmp::PartialEq for Direction { #[inline] fn eq(\u0026amp;self, other: \u0026amp;Direction) -\u0026gt; bool { let self_discr = ::core::intrinsics::discriminant_value(self); let other_discr = ::core::intrinsics::discriminant_value(other); self_discr == other_discr } } You can also implement the PartialEq trait manually:\nimpl PartialEq for Point { fn eq(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; bool { self.x == other.x \u0026amp;\u0026amp; self.y == other.y } } As mentioned earlier, the term \u0026ldquo;partial\u0026rdquo; in PartialEq does not imply that only part of a struct is compared. Moreover, implementing the PartialEq trait for only some fields of a type is not recommended. Let\u0026rsquo;s consider the following example.\nFrom the definition of the PartialEq trait, we can see that it takes a type parameter Rhs, which defaults to Self. This means you can use the PartialEq trait to compare two different types.\n// Define clothing types, deriving PartialEq to allow comparisons #[derive(PartialEq)] enum ClothingType { Shirt, Pants, Jacket, } // Define a Clothing struct with an ID and a clothing type struct Clothing { id: i32, clothing_type: ClothingType, } // Allow comparisons between Clothing and ClothingType impl PartialEq\u0026lt;ClothingType\u0026gt; for Clothing { fn eq(\u0026amp;self, other: \u0026amp;ClothingType) -\u0026gt; bool { self.clothing_type == *other } } // Allow comparisons between ClothingType and Clothing impl PartialEq\u0026lt;Clothing\u0026gt; for ClothingType { fn eq(\u0026amp;self, other: \u0026amp;Clothing) -\u0026gt; bool { *self == other.clothing_type } } fn main() { let my_clothing = Clothing { id: 101, clothing_type: ClothingType::Shirt, }; // Check if the clothing item is a shirt assert!(my_clothing == ClothingType::Shirt); // Check if the clothing item is not a jacket assert!(ClothingType::Jacket != my_clothing); } In this example, we define a Clothing struct that includes an ID and a ClothingType field. We then implement the PartialEq trait for both Clothing and ClothingType, enabling comparisons between these two types. However, the above example can be problematic as it may violate the transitivity property of equality. For instance, consider the following code:\n// Define clothing types, deriving PartialEq to allow comparisons #[derive(PartialEq)] enum ClothingType { Shirt, Pants, Jacket, } // Define a Clothing struct with an ID and a clothing type, deriving PartialEq #[derive(PartialEq)] struct Clothing { id: i32, clothing_type: ClothingType, } // Allow comparisons between Clothing and ClothingType impl PartialEq\u0026lt;ClothingType\u0026gt; for Clothing { fn eq(\u0026amp;self, other: \u0026amp;ClothingType) -\u0026gt; bool { self.clothing_type == *other } } // Allow comparisons between ClothingType and Clothing impl PartialEq\u0026lt;Clothing\u0026gt; for ClothingType { fn eq(\u0026amp;self, other: \u0026amp;Clothing) -\u0026gt; bool { *self == other.clothing_type } } fn main() { let c1 = Clothing { id: 101, clothing_type: ClothingType::Shirt, }; let c2 = Clothing { id: 102, clothing_type: ClothingType::Shirt, }; assert!(c1 == ClothingType::Shirt); assert!(ClothingType::Shirt == c2); // Check if the clothing items are equal assert!(c1 == c2); // This assertion will fail } In this scenario, equality comparisons can lead to unexpected results, breaking the transitivity rule. Transitivity requires that if a == b and b == c, then a == c must also hold true. Violating this property can cause logical inconsistencies and bugs in your code.\nIn most cases, it is advisable to derive the PartialEq trait for a struct or enum. If you choose to implement the trait manually, be careful to maintain the transitivity property to avoid logical inconsistencies.\nEq The Eq trait extends the PartialEq trait by enforcing reflexivity. It is defined as follows:\npub trait Eq: PartialEq { } The Eq trait does not introduce any new methods beyond those in PartialEq. Instead, it acts as a marker to signify that a type adheres to the reflexivity property of equality. This means that any type implementing Eq must ensure that every instance of the type is equal to itself.\nIt\u0026rsquo;s important to note that the Rust compiler cannot automatically verify the reflexivity property. Therefore, it is the programmer\u0026rsquo;s responsibility to ensure that the Eq trait is implemented correctly and that the reflexivity property is upheld.\nSimilar to the PartialEq trait, you can derive the Eq trait for a custom type:\n#[derive(Eq, PartialEq)] struct Point { x: i32, y: i32, } Note that you must also derive the PartialEq trait because Eq depends on it.\nPartialOrd The PartialOrd trait is used to compare values and determine their ordering. It is defined as follows:\npub trait PartialOrd\u0026lt;Rhs = Self\u0026gt;: PartialEq\u0026lt;Rhs\u0026gt; where Rhs: ?Sized, { // Required method fn partial_cmp(\u0026amp;self, other: \u0026amp;Rhs) -\u0026gt; Option\u0026lt;Ordering\u0026gt;; // Provided methods fn lt(\u0026amp;self, other: \u0026amp;Rhs) -\u0026gt; bool { ... } fn le(\u0026amp;self, other: \u0026amp;Rhs) -\u0026gt; bool { ... } fn gt(\u0026amp;self, other: \u0026amp;Rhs) -\u0026gt; bool { ... } fn ge(\u0026amp;self, other: \u0026amp;Rhs) -\u0026gt; bool { ... } } The lt, le, gt, and ge methods of this trait allow comparisons using the \u0026lt;, \u0026lt;=, \u0026gt;, and \u0026gt;= operators, respectively.\nThe partial_cmp method returns an Option\u0026lt;Ordering\u0026gt; enum, which can be Some(Ordering::Less), Some(Ordering::Equal), Some(Ordering::Greater), or None.\nYou might wonder why the PartialOrd trait returns an Option\u0026lt;Ordering\u0026gt; instead of an Ordering directly. The reason is similar to the PartialEq trait: it accounts for cases where values cannot be compared, such as NaN in floating-point numbers. Thus, partial_cmp returns None when a comparison is not possible.\nfn main() { let x = f32::NAN; let y = 5.0; assert_eq!(x.partial_cmp(\u0026amp;y), None); assert_eq!(y.partial_cmp(\u0026amp;x), None); } To implement the PartialOrd trait, you can either do it manually or derive it using the derive attribute.\nFor example, you can derive the PartialOrd trait for a struct like this:\n#[derive(PartialEq, PartialOrd)] struct Point { x: i32, y: i32, } Expanding the derive attribute generates the following implementation:\n#![feature(prelude_import)] #[prelude_import] use std::prelude::rust_2021::*; #[macro_use] extern crate std; struct Point { x: i32, y: i32, } ... // Implementation of PartialEq #[automatically_derived] impl ::core::cmp::PartialOrd for Point { #[inline] fn partial_cmp(\u0026amp;self, other: \u0026amp;Point) -\u0026gt; ::core::option::Option\u0026lt;::core::cmp::Ordering\u0026gt; { match ::core::cmp::PartialOrd::partial_cmp(\u0026amp;self.x, \u0026amp;other.x) { ::core::option::Option::Some(::core::cmp::Ordering::Equal) =\u0026gt; ::core::cmp::PartialOrd::partial_cmp(\u0026amp;self.y, \u0026amp;other.y), cmp =\u0026gt; cmp, } } } The comparison follows a lexicographical order, first comparing the x fields and then the y fields top to bottom.\nFor enums, variants are ordered by their discriminant values:\n#[derive(PartialEq, PartialOrd)] enum Direction { Up, Down, Left, Right, } assert!(Direction::Up \u0026lt; Direction::Down); The values of the variants are assigned discriminant values by default, starting from 0 for the first variant and incrementing by 1 for each subsequent variant.\nYou can verify the discriminant values of the variants using the std::mem::discriminant function:\nuse std::mem::discriminant; let up = Direction::Up; let down = Direction::Down; println!(\u0026#34;{:?}\u0026#34;, discriminant(\u0026amp;up)); // Output: Discriminant(0) println!(\u0026#34;{:?}\u0026#34;, discriminant(\u0026amp;down)); // Output: Discriminant(1) The derive attribute generates the following implementation:\n#![feature(prelude_import)] #[prelude_import] use std::prelude::rust_2021::*; #[macro_use] extern crate std; enum Direction { Up, Down, Left, Right, } ... // Implementation of PartialEq #[automatically_derived] impl ::core::cmp::PartialOrd for Direction { #[inline] fn partial_cmp(\u0026amp;self, other: \u0026amp;Direction) -\u0026gt; ::core::option::Option\u0026lt;::core::cmp::Ordering\u0026gt; { let __self_discr = ::core::intrinsics::discriminant_value(self); let __arg1_discr = ::core::intrinsics::discriminant_value(other); ::core::cmp::PartialOrd::partial_cmp(\u0026amp;__self_discr, \u0026amp;__arg1_discr) } } You can manually set the discriminant values for the variants to change the ordering:\n#[derive(PartialEq, PartialOrd)] enum Direction { Up = 4, Down = 3, Left = 2, Right = 1, } assert!(Direction::Up \u0026gt; Direction::Down); It is also possible to implement the PartialOrd trait manually:\nstruct Point { x: i32, y: i32, } impl PartialEq for Point { fn eq(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; bool { self.x == other.x \u0026amp;\u0026amp; self.y == other.y } } impl PartialOrd for Point { fn partial_cmp(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; Option\u0026lt;Ordering\u0026gt; { match self.x.partial_cmp(\u0026amp;other.x) { Some(Ordering::Equal) =\u0026gt; self.y.partial_cmp(\u0026amp;other.y), other =\u0026gt; other, } } } assert!(Point { x: 1, y: 2 } \u0026lt; Point { x: 2, y: 1 }); But be careful when implementing the PartialOrd trait manually. If you derive the PartialEq trait for a type, you should also derive the PartialOrd trait to ensure consistency between equality and ordering comparisons.\n#[derive(PartialEq)] struct Point { x: i32, y: i32, } impl PartialOrd for Point { fn partial_cmp(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; Option\u0026lt;Ordering\u0026gt; { Some(self.x.cmp(\u0026amp;other.x)) } } let p1 = Point { x: 1, y: 2 }; let p2 = Point { x: 1, y: 1 }; // `PartialEq` and `PartialOrd` are inconsistent assert!(p1.partial_cmp(\u0026amp;p2) == Some(Ordering::Equal)); assert!(p1 == p2); // This assertion will fail When you choose to implement the PartialOrd trait manually, it is advisable to implement all related traits (PartialEq, Eq, PartialOrd, and Ord) consistently and manually to ensure logical coherence.\nOrd The Ord trait builds upon the PartialOrd and Eq traits, ensuring that a type adheres to a total ordering. It is defined as follows:\npub trait Ord: Eq + PartialOrd { // Required method fn cmp(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; Ordering; // Provided methods fn max(self, other: Self) -\u0026gt; Self where Self: Sized { ... } fn min(self, other: Self) -\u0026gt; Self where Self: Sized { ... } fn clamp(self, min: Self, max: Self) -\u0026gt; Self where Self: Sized { ... } } The Ord trait enforces reflexivity through the Eq trait and provides the cmp method for comparing two values, returning an Ordering enum. This ensures that every pair of values can be compared, establishing a total order.\nThe implementation of the Ord trait must be consistent with the PartialOrd trait. Ord requires that the type also be PartialOrd, PartialEq, and Eq.\nYou can choose to derive it, or implement it manually. If you derive it, you should derive all four traits. If you implement it manually, you should manually implement all four traits, based on the implementation of Ord.\nstruct Point { x: i32, y: i32, } impl Ord for Point { fn cmp(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; Ordering { match self.x.cmp(\u0026amp;other.x) { Ordering::Equal =\u0026gt; self.y.cmp(\u0026amp;other.y), other =\u0026gt; other, } } } impl PartialOrd for Point { fn partial_cmp(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; Option\u0026lt;Ordering\u0026gt; { Some(self.cmp(other)) } } impl PartialEq for Point { fn eq(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; bool { self.x == other.x \u0026amp;\u0026amp; self.y == other.y } } impl Eq for Point {} When implementing PartialOrd, you can directly call the cmp method within the partial_cmp implementation. This approach ensures consistency between the PartialOrd and Ord implementations.\nReverse In certain situations, you might need to reverse the ordering of a type. For example, when using the BinaryHeap data structure, you may want to retrieve the smallest element instead of the largest. Rust provides the Reverse wrapper type to achieve this.\nuse std::cmp::Reverse; use std::collections::BinaryHeap; fn main() { let mut heap = BinaryHeap::new(); heap.push(Reverse(1)); heap.push(Reverse(3)); heap.push(Reverse(2)); assert_eq!(heap.pop(), Some(Reverse(1))); assert_eq!(heap.pop(), Some(Reverse(2))); assert_eq!(heap.pop(), Some(Reverse(3))); } When you want to access the value inside the Reverse wrapper, you can use .0 to access the inner value.\nlet rev = Reverse(5); assert_eq!(rev.0, 5); The Reverse type is a simple wrapper around a value that implements the Ord, PartialOrd, Eq, and PartialEq traits. It reverses the ordering of the inner value, allowing you to change the sorting behavior of a type.\npub struct Reverse\u0026lt;T\u0026gt;(pub T); Since this is all about ordering, there is no difference for PartialEq and Eq. However, for PartialOrd and Ord, the traits are implemented as follows:\nimpl\u0026lt;T: PartialOrd\u0026gt; PartialOrd for Reverse\u0026lt;T\u0026gt; { #[inline] fn partial_cmp(\u0026amp;self, other: \u0026amp;Reverse\u0026lt;T\u0026gt;) -\u0026gt; Option\u0026lt;Ordering\u0026gt; { other.0.partial_cmp(\u0026amp;self.0) } #[inline] fn lt(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; bool { other.0 \u0026lt; self.0 } #[inline] fn le(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; bool { other.0 \u0026lt;= self.0 } #[inline] fn gt(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; bool { other.0 \u0026gt; self.0 } #[inline] fn ge(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; bool { other.0 \u0026gt;= self.0 } } impl\u0026lt;T: Ord\u0026gt; Ord for Reverse\u0026lt;T\u0026gt; { #[inline] fn cmp(\u0026amp;self, other: \u0026amp;Reverse\u0026lt;T\u0026gt;) -\u0026gt; Ordering { other.0.cmp(\u0026amp;self.0) } } It simply reverses the order of the comparison. That\u0026rsquo;s all there is to it!\nConclusion This was a long post, but I hope it helped you understand the PartialEq, Eq, PartialOrd, Ord, and Reverse traits in Rust. Remember to use the derive attribute whenever possible to avoid inconsistencies.\n","permalink":"https://0xpoe.dev/posts/2025-01-11-rust-std-cmp/","summary":"\u003ch1 id=\"stdcmp\"\u003estd::cmp\u003c/h1\u003e\n\u003cp\u003eThe \u003ccode\u003estd::cmp\u003c/code\u003e module offers several traits for comparing values. It allows us to check for equality and determine the ordering of values. Unlike some other languages where the compiler automatically provides these implementations, Rust requires us to explicitly implement or derive these traits.\u003c/p\u003e\n\u003cp\u003eImplementing these traits can sometimes be challenging. In this post, we will explore the \u003ccode\u003ePartialEq\u003c/code\u003e, \u003ccode\u003eEq\u003c/code\u003e, \u003ccode\u003ePartialOrd\u003c/code\u003e, \u003ccode\u003eOrd\u003c/code\u003e, and \u003ccode\u003eReverse\u003c/code\u003e traits in detail.\u003c/p\u003e\n\u003ch2 id=\"partialeq\"\u003ePartialEq\u003c/h2\u003e\n\u003cp\u003eMany might assume that \u003ccode\u003ePartialEq\u003c/code\u003e is termed \u0026ldquo;partial\u0026rdquo; because it only compares certain parts of a type. However, the \u0026ldquo;partial\u0026rdquo; in \u003ccode\u003ePartialEq\u003c/code\u003e actually indicates that the equality comparison does not need to be reflexive \u003ccode\u003e(a == a)\u003c/code\u003e. In other words, \u003ccode\u003ePartialEq\u003c/code\u003e does not require a type to always be equal to itself.\u003c/p\u003e","title":"Rust equality and ordering"},{"content":"Background Recently, we have been tackling the challenge of supporting 3 million tables within a single TiDB cluster. One of the most significant hurdles we\u0026rsquo;ve faced is optimizing the performance of statistics collection. In its current implementation, TiDB gathers basic table information from all servers and consolidates it into a single system table. While functional, this approach becomes highly inefficient when managing millions of tables, consuming excessive CPU and taking a considerable amount of time.\nIn this blog post, I’ll introduce a new batch processing approach for writing table information to the system table. This method not only improves efficiency but also significantly reduces resource consumption. Let\u0026rsquo;s dive in!\nStatistics Delta What is Statistics Delta? Statistics play a crucial role in optimizing query plans. In TiDB, the owner node is responsible for collecting statistics and storing them in system tables. To ensure that the statistics accurately reflect the current data distribution, they need to be updated periodically. This raises an important question: When is the optimal time to collect these statistics?\nIn TiDB, we primarily use Data Manipulation Language (DML) changes to determine when to update the statistics. This method is known as the statistics delta. The statistics delta consists of three fields: modify_count, count, and version. The modify_count field tracks the number of changes made to the table (DELETE/INSERT/UPDATE), the count field records the total number of rows in the table, and the version field identifies the version of the statistics.\nTiDB stores the statistics delta in the mysql.stats_meta table. This table contains a row for each table in the database, with each row holding the statistics delta for that table. For example, when you insert a row into a table, the modify_count field increases by one, and the count field also increases by one. Conversely, when you delete a row, the modify_count field increases by one, but the count field decreases by one. When you update a row, the modify_count field increases by one, while the count field remains unchanged.\nAfter collecting the statistics delta, TiDB owner nodes use this information to initiate the statistics collection process. Therefore, it is crucial to ensure that the statistics delta is as accurate and up-to-date as possible.\nHow to collect statistics delta? Since every node in the TiDB cluster can modify the data, it is necessary to collect the statistics delta from all nodes. To accomplish this, we use a single system table to store the statistics delta. However, it is not feasible to keep the statistics delta in the system table updated in real-time. Therefore, we periodically update the statistics delta in the system table to ensure it remains as current as possible.\nIn the current implementation, whenever TiDB commits a transaction, it updates the statistics delta for the current session. Every 2 minutes, TiDB aggregates these deltas and writes them to the system table. Each TiDB server can have many sessions, so we need to collect statistics changes from all sessions. TiDB uses a linked list to store the statistics delta for each session. When it is time to dump the statistics delta to the system table, TiDB traverses the linked list to gather the deltas from each session.\ntype TableDelta struct { Delta int64 Count int64 ColSize map[int64]int64 InitTime time.Time // InitTime is the time that this delta is generated. TableID int64 } type TableDelta struct { delta map[int64]variable.TableDelta // map[tableID]delta lock sync.Mutex } type SessionStatsItem struct { mapper *TableDelta next *SessionStatsItem sync.Mutex // deleted is set to true when a session is closed. deleted bool } As illustrated in the code snippet above, the current implementation employs a linked list to store the statistics delta for each session, maintaining it in memory. Each session directly updates the table delta via the SessionStatsItem. When a session is closed, the deleted flag is set to true, and the session is subsequently removed from the linked list, ensuring it is no longer utilized.\nTo manage the SessionStatsItem linked list, TiDB utilizes the SessionStatsList structure. This structure includes a pointer to the head of the linked list. Additionally, the SessionStatsList structure offers a method to traverse the linked list, consolidating the statistics delta from each session into a unified delta.\ntype SessionStatsList struct { // tableDelta contains all the delta map from collectors when we dump them to KV. tableDelta *TableDelta ... // other fields // listHead contains all the stats collector required by session. listHead *SessionStatsItem } // SweepSessionStatsList will loop over the list, merge each session\u0026#39;s local stats into handle // and remove closed session\u0026#39;s collector. func (sl *SessionStatsList) SweepSessionStatsList() { deltaMap := NewTableDelta() ... prev := sl.listHead prev.Lock() for curr := prev.next; curr != nil; curr = curr.next { curr.Lock() // Merge the session stats into deltaMap respectively. merge(curr, deltaMap, colMap) if curr.deleted { prev.next = curr.next curr.Unlock() } else { prev.Unlock() prev = curr } } prev.Unlock() sl.tableDelta.Merge(deltaMap.GetDeltaAndReset()) ... } Here is an example of the in-memory data for each session:\nAfter merging the statistics delta from each session, TiDB writes the aggregated delta to the system table.\nfunc (s *statsUsageImpl) DumpStatsDeltaToKV(dumpAll bool) error { ... s.SweepSessionStatsList() deltaMap := s.SessionTableDelta().GetDeltaAndReset() defer func() { s.SessionTableDelta().Merge(deltaMap) }() return utilstats.CallWithSCtx(s.statsHandle.SPool(), func(sctx sessionctx.Context) error { is := sctx.GetDomainInfoSchema().(infoschema.InfoSchema) currentTime := time.Now() for id, item := range deltaMap { if !s.needDumpStatsDelta(is, dumpAll, id, item, currentTime) { continue } updated, err := s.dumpTableStatCountToKV(is, id, item) if err != nil { return errors.Trace(err) } if updated { UpdateTableDeltaMap(deltaMap, id, -item.Delta, -item.Count, nil) } if err = storage.DumpTableStatColSizeToKV(sctx, id, item); err != nil { delete(deltaMap, id) return errors.Trace(err) } if updated { delete(deltaMap, id) } else { m := deltaMap[id] m.ColSize = nil deltaMap[id] = m } } return nil }) } However, this function is inefficient. It processes each item individually, which is both time-consuming and resource-intensive. Each item requires a separate transaction, and each transaction incurs overhead due to the need to lock the table and write data to storage.\nPerformance Issues After testing the current implementation with 3 million tables, we observed significant performance issues. In high-load clusters, the statistics delta is updated frequently, generating a large volume of transactions that degrade performance. The root cause lies in the DumpStatsDeltaToKV function, where each execution triggers query compilation over 100,000 times, resulting in substantial CPU overhead.\nExamining the Stats Meta Updating Duration metric reveals that the time required to update the statistics delta is excessively high.\nFurther analysis of the CPU profile reveals that the DumpStatsDeltaToKV function is a major contributor to CPU consumption. A significant portion of the CPU time is spent on the compile function, highlighting it as a critical bottleneck.\nIt executes the following function over 100,000 times:\n// UpdateStatsMeta update the stats meta stat for this Table. func UpdateStatsMeta( ctx context.Context, sctx sessionctx.Context, startTS uint64, delta variable.TableDelta, id int64, isLocked bool, ) (err error) { ... // use INSERT INTO ... ON DUPLICATE KEY UPDATE here to fill missing stats_meta. _, err = statsutil.ExecWithCtx(ctx, sctx, \u0026#34;insert into mysql.stats_meta (version, table_id, modify_count, count) values (%?, %?, %?, 0) on duplicate key \u0026#34;+ \u0026#34;update version = values(version), modify_count = modify_count + values(modify_count), count = if(count \u0026gt; %?, count - %?, 0)\u0026#34;, startTS, id, delta.Count, -delta.Delta, -delta.Delta) ... return err } To address these performance issues, we need to find a more efficient way to update the statistics delta without processing each item individually. However, we must ensure that this solution does not consume additional CPU and memory resources. Therefore, simply adding more concurrent workers to process the items in parallel is not a viable option.\nBatch Dumping Statistics Delta Design To enhance the performance of updating the statistics delta, we propose a new approach: batch dumping statistics delta. This method aggregates the statistics delta from all sessions and writes it to the system table in a single transaction/query. By batching the updates, we can significantly reduce the number of transactions required, thereby improving performance and reducing resource consumption.\nWe change above UpdateStatsMeta function to take a slice of TableDelta as input, and update the statistics delta in a single transaction.\ntype DeltaUpdate struct { Delta variable.TableDelta TableID int64 IsLocked bool } func UpdateStatsMeta( ctx context.Context, sctx sessionctx.Context, startTS uint64, updates ...*DeltaUpdate, ) (err error) { ... } Within the UpdateStatsMeta function, we continue to use the INSERT INTO ... ON DUPLICATE KEY UPDATE statement to update the statistics delta. However, we now specify multiple values in the INSERT INTO statement, allowing us to update the statistics delta in a single transaction.\nsql := fmt.Sprintf(\u0026#34;insert into mysql.stats_meta (version, table_id, modify_count, count) values %s \u0026#34;+ \u0026#34;on duplicate key update version = values(version), modify_count = modify_count + values(modify_count), \u0026#34;+ \u0026#34;count = count + values(count)\u0026#34;, strings.Join(unlockedPosValues, \u0026#34;,\u0026#34;)) if _, err = statsutil.ExecWithCtx(ctx, sctx, sql); err != nil { return err } Test Results I have submitted a PR to implement this new approach. After testing the batch dumping statistics delta method, we observed a significant improvement in performance. You can view the PR here.\nThe duration dropped from 40 minutes to 20 seconds, as shown in the following screenshot:\nIn the CPU profile, the DumpStatsDeltaToKV function no longer consumes a significant amount of CPU time. The compile function is no longer a bottleneck, as the number of executions has been reduced to a more manageable level.\nTuning the Batch Size To achieve optimal performance, it is essential to identify the ideal batch size for updating the statistics delta. Through testing various batch sizes, we determined that a batch size of 100,000 items delivers the best results. This configuration balances efficiency and resource utilization, ensuring the update process is both fast and resource-efficient.\nBatch Size Duration Metric 5000 ~40 sec Stats Meta Updating Duration 10000 20-40 sec; Most 40 sec Stats Meta Updating Duration 50000 20-40 sec; Most 20 sec Stats Meta Updating Duration 100000 ~20 sec Stats Meta Updating Duration Conclusion The new batch dumping approach for the statistics delta has significantly enhanced TiDB statistics update performance. This method aggregates deltas from all sessions into a single transaction, minimizing overhead and resource consumption while ensuring an efficient update process, even in large-scale deployments.\n","permalink":"https://0xpoe.dev/posts/2024-12-14-batch-dumping-stats-delta/","summary":"\u003ch1 id=\"background\"\u003eBackground\u003c/h1\u003e\n\u003cp\u003eRecently, we have been tackling the challenge of supporting 3 million tables within a single TiDB cluster. One of the most significant hurdles we\u0026rsquo;ve faced is optimizing the performance of statistics collection. In its current implementation, TiDB gathers basic table information from all servers and consolidates it into a single system table. While functional, this approach becomes highly inefficient when managing millions of tables, consuming excessive CPU and taking a considerable amount of time.\u003c/p\u003e","title":"Batch Dumping Statistics Delta"},{"content":" ","permalink":"https://0xpoe.dev/presentations/2024-12-14-tidb-analyze/","summary":"\u003ciframe src=\"https://tidb-analyze.slides.0xpoe.dev/\" width=\"100%\" height=\"500px\" frameborder=\"0\" allowfullscreen\u003e\u003c/iframe\u003e","title":"TiDB Analyze"},{"content":"Background TiDB provides the analyze table table_name command to generate statistics for tables. When analyzing partitioned tables, TiDB processes each partition independently and in parallel. Once analysis completes, TiDB aggregates the individual partition statistics into a single global statistics object for the entire table.\nThe analyze table command has two concurrency-related parameters:\ntidb_build_stats_concurrency: Determines how many partitions can be processed simultaneously during statistics collection tidb_analyze_partition_concurrency: Controls parallel workers for saving partition statistics However, these parameters have several drawbacks:\nThe name tidb_build_stats_concurrency is imprecise - it actually governs parallel processing of partitions/tables rather than statistics building itself Having two separate concurrency controls is unnecessarily complex and makes it difficult for users to understand and predict the performance impact of their settings In this blog post, we will discuss the issues with the current parameters and propose a solution to merge them into one.\nTest Result Small Partition Table To evaluate the performance impact of these parameters, I benchmarked an analyze table command on a partitioned table containing 100 partitions and 30 million rows. Here are the results:\nNote: Test environment: 3 TiKV nodes and 3 TiDB nodes, each with 16 cores and 32GB RAM.\ntidb_build_stats_concurrency tidb_analyze_partition_concurrency analyze table Time 2 2 6 min 8.61 sec 15 2 2 min 55.71 sec 2 15 6 min 4.41 sec 15 15 2 min 26.63 sec The benchmark results clearly show that tidb_build_stats_concurrency has a much larger impact on overall execution time than tidb_analyze_partition_concurrency.\nThis makes sense given the small partition sizes (300k rows per partition) - analyzing each partition completes quickly and generates minimal statistics data to save. As a result, the parallel statistics saving controlled by tidb_analyze_partition_concurrency isn\u0026rsquo;t a performance bottleneck, explaining its minimal impact on total execution time.\nThe following sections will prove this point by showing the analyze_jobs table.\nAnalysis Before we dive into the details, let\u0026rsquo;s first take a look at the analysis model of TiDB.\nCollection Phase Here\u0026rsquo;s how TiDB\u0026rsquo;s analyze workflow works:\nThe Analyze Plan Builder splits the work into tasks by table/partition Multiple analyze workers run these tasks in parallel Each worker: Processes its assigned data independently Streams results back to a central handler Updates system tables with statistics as it goes After all workers finish: Global statistics are merged if needed The statistics cache gets refreshed This parallel architecture enables efficient and reliable statistics collection across the TiDB cluster. The tidb_build_stats_concurrency parameter plays a crucial role here by determining how many analyze workers can run simultaneously, directly impacting the throughput of partition processing.\nPersistence Phase Once data collection completes, TiDB enters the persistence phase. During this phase, the system writes the collected statistics to some system tables: mysql.stats_meta, mysql.stats_buckets, etc. The tidb_analyze_partition_concurrency parameter determines how many concurrent workers handle these write operations.\nThese parameters have different but related roles. The system follows a producer-consumer pattern, with tidb_build_stats_concurrency as the producer controlling statistics collection, and tidb_analyze_partition_concurrency as the consumer managing persistence. This design means their relative impact on performance can vary significantly depending on the specific workload characteristics.\nNow that we understand both phases, let\u0026rsquo;s dive into the execution details of the analyze table command to see how it works in practice.\nTiDB records the execution details of the analyze table command in the mysql.analyze_jobs table. The following is an example of the analyze_jobs table:\nSELECT table_schema, table_name, partition_name, job_info, processed_rows, start_time, end_time, state FROM mysql.analyze_jobs LIMIT 3; +------------+----------+--------------+-------------------------------------------------------------------------------------------------------------+--------------+-------------------+-------------------+--------+ |table_schema|table_name|partition_name|job_info |processed_rows|start_time |end_time |state | +------------+----------+--------------+-------------------------------------------------------------------------------------------------------------+--------------+-------------------+-------------------+--------+ |test |test_table|p18 |auto analyze table all indexes, columns id, part_id with 256 buckets, 100 topn, 1 samplerate |413000 |2024-11-18 15:53:04|2024-11-18 15:53:05|finished| |test |test_table|p6 |auto analyze table all indexes, columns id, part_id with 256 buckets, 100 topn, 0.2722772277227723 samplerate|425000 |2024-11-18 15:53:05|2024-11-18 15:53:06|finished| |test |test_table|p0 |auto analyze table all indexes, columns id, part_id with 256 buckets, 100 topn, 0.2736318407960199 samplerate|434000 |2024-11-18 15:53:05|2024-11-18 15:53:06|finished| +------------+----------+--------------+-------------------------------------------------------------------------------------------------------------+--------------+-------------------+-------------------+--------+ Let\u0026rsquo;s analyze the overall timeline from when statistics collection begins to when the data is persisted to storage. This will give us insight into both the collection and persistence phases of the operation.\ntidb_build_stats_concurrency tidb_analyze_partition_concurrency partition Start Time End Time 2 2 p18 2024-11-22 11:31:27 2024-11-22 11:31:36 15 2 p18 2024-11-22 12:37:35 2024-11-22 12:38:14 Examining the start_time and end_time columns reveals that individual partition processing times remain consistent across different parameter configurations, with all partitions completing within a 10-second window. This consistent timing indicates that the overall performance differences we observed cannot be explained by variations in how long it takes to process each partition.\nHowever, examining the number of concurrent partition operations reveals that tidb_build_stats_concurrency directly controls the degree of parallelism in partition processing.\nSELECT COUNT(*) AS record_count FROM mysql.analyze_jobs WHERE start_time BETWEEN \u0026#39;2024-11-22 11:31:00\u0026#39; AND \u0026#39;2024-11-22 11:31:59\u0026#39;; tidb_build_stats_concurrency tidb_analyze_partition_concurrency record_count 2 2 20 15 2 29 The analysis reveals that increasing tidb_build_stats_concurrency enables more partitions to be processed concurrently. Since statistics persistence is not a performance bottleneck, this higher degree of partition-level parallelism directly translates to faster overall statistics collection.\nThis finding explains the significant performance impact of tidb_build_stats_concurrency compared to the relatively minor effect of tidb_analyze_partition_concurrency.\nLarge Partition Table I also tested the analyze table command on a partitioned table containing 100 partitions and 2 billion rows. The results are as follows:\nNote: Test environment: 3 TiKV nodes and 3 TiDB nodes, each with 16 cores and 32GB RAM. Same as the previous test.\ntidb_build_stats_concurrency tidb_analyze_partition_concurrency analyze table Time 2 2 25 min 39.67 sec 15 2 10 min 58.74 sec 2 15 24 min 15.95 sec 15 15 9 min 4.36 sec The results from this large-scale test validate our earlier findings - increasing tidb_build_stats_concurrency provides significant performance improvements, while tidb_analyze_partition_concurrency has a relatively minor impact on overall execution time.\nWhile our initial findings suggest that tidb_build_stats_concurrency is the dominant factor in performance optimization, let\u0026rsquo;s examine a different scenario to validate this hypothesis.\nWide Table I tested the analyze table command on a wide table containing 500 partitions and 200 columns and 3 million rows. Here are the results:\ntidb_build_stats_concurrency tidb_analyze_partition_concurrency analyze table Time 2 2 1 hour 17 min 57.55 sec 15 2 1 hour 15 min 30.46 sec 2 15 34 min 31.38 sec 15 15 34 min 56.99 sec The execution time is influenced by both parameters, with tidb_analyze_partition_concurrency having a more significant impact compared to the small partition table test. This is because the increased number of columns creates a bottleneck during the statistics persistence phase.\nThese settings are hard to tune correctly. Optimal configuration requires deep understanding of both the collection and persistence phases for your specific workload. Additionally, since these parameters are cluster-wide settings rather than table-specific, finding ideal values that work well across different table schemas becomes challenging.\nConclusion From our tests, we can see that adjusting these two parameters is actually quite challenging. While tidb_build_stats_concurrency dominates performance for partitioned tables, our wide table test shows that tidb_analyze_partition_concurrency can have a more significant impact in certain scenarios. To simplify configuration, I recommend merging these parameters. Since tidb_analyze_partition_concurrency better describes the overall functionality of controlling parallelism during statistics collection, we should keep this parameter and deprecate tidb_build_stats_concurrency. This will make tuning more straightforward while still providing the necessary control over concurrency for different table types.\n","permalink":"https://0xpoe.dev/posts/2024-11-18-merge-tidb_build_stats_concurrency-and-tidb_analyze_partition_concurrency/","summary":"\u003ch1 id=\"background\"\u003eBackground\u003c/h1\u003e\n\u003cp\u003eTiDB provides the \u003ccode\u003eanalyze table table_name\u003c/code\u003e command to generate statistics for tables. When analyzing partitioned tables, TiDB processes each partition independently and in parallel. Once analysis completes, TiDB aggregates the individual partition statistics into a single global statistics object for the entire table.\u003c/p\u003e\n\u003cp\u003eThe \u003ccode\u003eanalyze table\u003c/code\u003e command has two concurrency-related parameters:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003etidb_build_stats_concurrency\u003c/code\u003e: Determines how many partitions can be processed simultaneously during statistics collection\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003etidb_analyze_partition_concurrency\u003c/code\u003e: Controls parallel workers for saving partition statistics\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eHowever, these parameters have several drawbacks:\u003c/p\u003e","title":"Simplifying TiDB Statistics Collection: Unifying Concurrency Controls"},{"content":"Reminder I am working on merging the cargo-info subcommand into Cargo. There are a few points that need to be addressed before merging. I would like to get feedback on these points. You can find the key points of concern at the end of this post.\nBackground This adds a new subcommand to Cargo, cargo info. This subcommand would allow users to get information about a crate from the command line, without having to go to the web.\nThe main motivation for this is to make it easier to get information about a crate from the command line. Currently, the way to get information about a crate is to go to the web and look it up on crates.io or find the crate\u0026rsquo;s source code and look at the Cargo.toml file. This is not very convenient, especially not all information is displayed on the crates.io page. This command also has been requested by the community for a long time. You can find more discussion about this in cargo#948.\nAnother motivation is to make the workflow of finding and evaluating crates more efficient. In the current workflow, users can search for crates using cargo search, but then they have to go to the web to get more information about the crate. This is not very efficient, especially if the user is just trying to get a quick overview of the crate. This would allow users to quickly get information about a crate without having to leave the terminal.\nExample usage:\n$ cargo info clap Updating crates.io index clap #argument #cli #arg #parser #parse A simple to use, efficient, and full-featured Command Line Argument Parser version: 4.4.18 license: MIT OR Apache-2.0 rust-version: 1.70.0 documentation: https://docs.rs/clap/4.4.18 repository: https://github.com/clap-rs/clap features: default = [std, color, help, usage, error-context, suggestions] std = [clap_builder/std] color = [clap_builder/color] help = [clap_builder/help] usage = [clap_builder/usage] error-context = [clap_builder/error-context] suggestions = [clap_builder/suggestions] cargo = [clap_builder/cargo] debug = [clap_builder/debug, clap_derive?/debug] deprecated = [clap_builder/deprecated, clap_derive?/deprecated] derive = [dep:clap_derive] env = [clap_builder/env] string = [clap_builder/string] unicode = [clap_builder/unicode] unstable-doc = [clap_builder/unstable-doc, derive] unstable-styles = [clap_builder/unstable-styles] unstable-v5 = [clap_builder/unstable-v5, clap_derive?/unstable-v5, deprecated] wrap_help = [clap_builder/wrap_help] dependencies: clap_builder@=4.4.18 clap_derive@=4.4.7 owners: kbknapp (Kevin K.) github:rust-cli:maintainers (Maintainers) github:clap-rs:admins (Admins) Detailed design Content Explanation Why clap Name The basic information. #argument #cli #arg #parser #parse Keywords It\u0026rsquo;s more like a category, which you can use to search for relevant alternatives. A simple to use, efficient, and full-featured Command Line Argument Parser Description The basic information. version: 4.4.18 Version The basic information. license: MIT OR Apache-2.0 License When choosing a crate, it is crucial to consider the license. rust-version: 1.70.0 MSRV When choosing a crate, it is crucial to make sure it can work with your MSRV. documentation: https://docs.rs/clap/4.4.18 Documentation Link Use these links can find more docs and information. repository: https://github.com/clap-rs/clap Repo Link Use these links can find more docs and information. features: Default Features And Other Features It helps for enabling features. dependencies: All dependencies It indicates what it depends on. owners: Owners It indicates who maintains the crate. Some important notes Downloading the crate from any Cargo compatible registry The cargo info command will download the crate from any Cargo compatible registry. It will then extract the information from the Cargo.toml file and display it in the terminal.\nIf the crate is already in the local cache, it will not download the crate again. It will get the information from the local cache.\nPick the correct version from the workspace When executed in a workspace directory, the cargo info command chooses the version that the workspace is currently using.\nIf there\u0026rsquo;s a lock file available, the version from this file will be used. In the absence of a lock file, the command attempts to select a version that is compatible with the Minimum Supported Rust Version (MSRV). And the lock file will be generated automatically.\nThe following hierarchy is used to determine the MSRV:\nFirst, the MSRV of the parent directory package is checked, if it exists. If the parent directory package does not specify an MSRV, the minimal MSRV of the workspace is checked. If neither the workspace nor the parent directory package specify an MSRV, the version of the current Rust compiler (rustc \u0026ndash;version) is used. Prior art NPM npm has a similar command called npm info. For example:\n$ npm info lodash lodash@4.17.21 | MIT | deps: none | versions: 114 Lodash modular utilities. https://lodash.com/ keywords: modules, stdlib, util dist .tarball: https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz .shasum: 679591c564c3bffaae8454cf0b3df370c3d6911c .integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== .unpackedSize: 1.4 MB maintainers: - mathias \u0026lt;mathias@qiwi.be\u0026gt; - jdalton \u0026lt;john.david.dalton@gmail.com\u0026gt; - bnjmnt4n \u0026lt;benjamin@dev.ofcr.se\u0026gt; dist-tags: latest: 4.17.21 published over a year ago by bnjmnt4n \u0026lt;benjamin@dev.ofcr.se\u0026gt; Poetry Poetry has a similar command called poetry show.\nFor example:\n$ poetry show pendulum name : pendulum version : 1.4.2 description : Python datetimes made easy dependencies - python-dateutil \u0026gt;=2.6.1 - tzlocal \u0026gt;=1.4 - pytzdata \u0026gt;=2017.2.2 required by - calendar \u0026gt;=1.4.0 Known Points of Concern Report crates metrics? cargo-information#20\nProposal:\nrecent download count (popularity) last updated (give a feel for how active development is) Only for crates.io. What dependency fields might be relevant to indicate? cargo-information#23\nProposal:\nFrom @epage: Dependencies are mostly an implementation detail (except public) but people sometimes care, so I figure that holding off on private dependencies to \u0026ndash;verbose might buy us more space. From @0xPoe: How about we show all the dependencies and only show the dev-dependencies and build-dependencies for a \u0026ndash;verbose. I guess checking its dependencies before you use it in your project would always be considered. How should we render features? cargo-information#26\nProposal: Currently, it\u0026rsquo;s a simple list of features and their dependencies. We could consider a tree view:\nfeatures: parent1 child1 parent2 child1* What version should we default to within a workspace? What if it isn\u0026rsquo;t the direct dependency but is a transitive dependency? cargo-information#29\n","permalink":"https://0xpoe.dev/posts/2024-02-01-feedback-on-cargo-info/","summary":"\u003ch1 id=\"reminder\"\u003eReminder\u003c/h1\u003e\n\u003cp\u003eI am working on merging the \u003ccode\u003ecargo-info\u003c/code\u003e subcommand into Cargo. There are a few points that need to be addressed before merging. I would like to get feedback on these points.\nYou can find the key points of concern at the end of this post.\u003c/p\u003e\n\u003ch1 id=\"background\"\u003eBackground\u003c/h1\u003e\n\u003cp\u003eThis adds a new subcommand to Cargo, \u003ccode\u003ecargo info\u003c/code\u003e. This subcommand would allow users to get information about a crate from the command line, without having to go to the web.\u003c/p\u003e","title":"Feedback on `cargo-info` to prepare it for merging"},{"content":" ","permalink":"https://0xpoe.dev/presentations/2023-09-24-cargo-index/","summary":"\u003cdiv style=\"position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;\"\u003e\n      \u003ciframe allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen\" loading=\"eager\" referrerpolicy=\"strict-origin-when-cross-origin\" src=\"https://www.youtube.com/embed/QW4f0xraBuU?autoplay=0\u0026amp;controls=1\u0026amp;end=0\u0026amp;loop=0\u0026amp;mute=0\u0026amp;start=0\" style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;\" title=\"YouTube video\"\u003e\u003c/iframe\u003e\n    \u003c/div\u003e\n\n\u003chr\u003e\n\u003ciframe src=\"https://cargo-index.slides.0xpoe.dev/\" width=\"100%\" height=\"500px\" frameborder=\"0\" allowfullscreen\u003e\u003c/iframe\u003e","title":"Cargo: From Git Index to Sparse Index"},{"content":"Recently we released Rustup 1.26.0, which includes a bunch of new features and bug fixes. We also upgraded the clap version to 3.2.25, which is a major version upgrade. This upgrade was a bit tricky, because clap 3.0.0 had a lot of breaking changes. We needed to make sure that the new version of Rustup worked as expected. So before we upgraded clap we added UI tests for Rustup. We use trycmd to test the CLI of Rustup. In this post I will show you how to use trycmd to test your CLI.\nWhy trycmd? Because it is very easy to use and well developed. It is also very easy to integrate with CI. We can use cargo test to run the test cases. Most importantly, trcmd is recommended by the clap team. You can find its document in the clap upgrade guide.\nWhat is trycmd? From the README of trycmd: \u0026ldquo;trycmd is a test harness that will enumerate test case files and run them to verify the results, taking inspiration from trybuild and cram.\u0026rdquo;\nHere is an example:\n// tests/cli_tests.rs #[test] fn cli_tests() { trycmd::TestCases::new() .case(\u0026#34;tests/cmd/*.toml\u0026#34;) .case(\u0026#34;README.md\u0026#34;); } We can use trycmd::TestCases::new() to create a new test case. Then we can use .case() to add test cases. The argument of .case() is a glob pattern. It will enumerate all the files that match the pattern and run them as test cases.\nIn this example, we used tests/cmd/*.toml to match all the files in tests/cmd that end with .toml. We also use README.md as a test case. The README.md file is a markdown file, but trycmd will treat it as a test case. It will run the command in the code block and verify the output, which is a very useful feature when you want to test the examples in your README.\nWe can treat this test case as a normal test case and run it with cargo test.\nHow to write a test case? We have two types of test cases available: TOML files and Markdown files. The TOML file provides more flexibility compared to the Markdown file. It allows us to test command line arguments, command output, and even the exit code of the command. On the other hand, the Markdown file is a simpler option that allows us to focus on testing the output of the command.\nIn clap documentation, you can find a simple example of trycmd. But it only tests a Markdown file. In this post, I will show you how to write a TOML file and a Markdown file test. You can find more examples from some real projects, like typos tests, clap tests and rustup tests.\nTOML file Here is an example of a TOML file:\n# tests/cmd/help.toml bin.name = \u0026#34;YOUR_BIN_NAME\u0026#34; args = [\u0026#34;--help\u0026#34;] status.code = 0 stdout = \u0026#34;\u0026#34;\u0026#34; HELP MESSAGE \u0026#34;\u0026#34;\u0026#34; stderr = \u0026#34;\u0026#34; We can use bin.name for the binary name, args for command arguments, status.code for the exit code, and stdout and stderr for command output.\nIf your CLI requires reading input files, you can organize them in a separate directory with the same name but with a .in suffix. For instance, if your CLI needs to read a file called input.txt and your test case is located at tests/cmd/input.toml, you can place the input.txt file in the directory tests/cmd/input.in/input.txt. This naming convention helps to distinguish input files from other files and maintain a structured organization for your test cases.\n# tests/cmd/input.toml bin.name = \u0026#34;YOUR_BIN_NAME\u0026#34; status.code = 0 stdout = \u0026#34;\u0026#34; stderr = \u0026#34;\u0026#34; tree tests/cmd . ├── input.toml └── input.in └── input.txt If your CLI involves writing output files, you can utilize a directory with the same name as the test file but with a .out suffix to store the generated output files. For instance, if your CLI reads the input.txt file and writes the output.txt file, you can place the output.txt file in the directory tests/cmd/input.out/output.txt.\ntree tests/cmd . ├── input.toml ├── input.in │ └── input.txt └── input.out ├── input.txt └── output.txt trycmd will compare the output file with the expected output file. If they are different, the test will fail.\nMarkdown file In markdown files, we can utilize either the console or trycmd code block to represent test cases. Here\u0026rsquo;s an example:\n```console $ command ... ``` Or ```trycmd $ command ... ``` Sometimes, your test might include output that is generated at runtime. When that\u0026rsquo;s the case, you can use variables to replace those values.\n```console $ simple \u0026#34;blah blah runtime-value blah\u0026#34; Hello blah blah [REPLACEMENT] blah! ``` In this example, we used [REPLACEMENT] to indicate the runtime value. We can use trycmd::TestCases::new().case(\u0026quot;README.md\u0026quot;).insert_var(\u0026quot;REPLACEMENT\u0026quot;, \u0026quot;runtime-value\u0026quot;) to replace the runtime value.\n$ simple \u0026#34;blah blah runtime-value blah\u0026#34; Hello blah blah runtime-value blah! The console code block will substitute [REPLACEMENT] with the value of the REPLACEMENT variable. The test will pass if the output is Hello blah blah runtime-value blah!. If the output differs from this expected value, the test will fail.\nOne Real Example Create a CLI Use cargo new to create a new CLI.\ncargo new --bin trycmd-example Add clap as a dependency with derive feature.\ncargo add clap --features derive Check the Cargo.toml file. You should see the clap dependency.\n# Cargo.toml [package] name = \u0026#34;trycmd-example\u0026#34; version = \u0026#34;0.1.0\u0026#34; edition = \u0026#34;2021\u0026#34; # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] clap = { version = \u0026#34;4.3.10\u0026#34;, features = [\u0026#34;derive\u0026#34;] } Add a simple CLI.\n// src/main.rs use clap::Parser; /// Simple program to greet a person #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { /// Name of the person to greet #[arg(short, long)] name: String, /// Number of times to greet #[arg(short, long, default_value_t = 1)] count: u8, } fn main() { let args = Args::parse(); for _ in 0..args.count { println!(\u0026#34;Hello {}!\u0026#34;, args.name) } } Build the CLI to make sure it works.\ncargo build ./target/debug/trycmd-example --help Get the output:\nSimple program to greet a person Usage: trycmd-example [OPTIONS] --name \u0026lt;NAME\u0026gt; Options: -n, --name \u0026lt;NAME\u0026gt; Name of the person to greet -c, --count \u0026lt;COUNT\u0026gt; Number of times to greet [default: 1] -h, --help Print help -V, --version Print version Now we have a simple CLI. Let\u0026rsquo;s add some test cases.\nAdd a TOML test case Create a tests/cmd directory.\nmkdir -p tests/cmd tree . --gitignore . ├── Cargo.lock ├── Cargo.toml ├── src │ └── main.rs └── tests └── cmd Create a tests/cmd/help.toml file.\ntouch tests/cmd/help.toml # tests/cmd/help.toml bin.name = \u0026#34;trycmd-example\u0026#34; args = [\u0026#34;--help\u0026#34;] status.code = 0 stdout = \u0026#34;\u0026#34; stderr = \u0026#34;\u0026#34; Add trycmd as a dev dependency.\ncargo add trycmd --dev Check the Cargo.toml file. You should see the trycmd dependency.\n# Cargo.toml [package] name = \u0026#34;trycmd-example\u0026#34; version = \u0026#34;0.1.0\u0026#34; edition = \u0026#34;2021\u0026#34; # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] clap = { version = \u0026#34;4.3.10\u0026#34;, features = [\u0026#34;derive\u0026#34;] } [dev-dependencies] # \u0026lt;-- Add this section trycmd = \u0026#34;0.14.16\u0026#34; # \u0026lt;-- Add this line Add a Rust test case.\ntouch tests/cmd.rs // tests/cmd.rs #[test] fn test_cmd() { let t = trycmd::TestCases::new(); let trycmd_example_binary = trycmd::cargo::cargo_bin(\u0026#34;trycmd-example\u0026#34;); t.register_bin(\u0026#34;trycmd-example\u0026#34;, \u0026amp;trycmd_example_binary); t.case(\u0026#34;tests/cmd/*.toml\u0026#34;); } For this test case, we start by creating a new test case using trycmd::TestCases::new(). Next, we obtain the path of the trycmd-example binary by utilizing trycmd::cargo::cargo_bin(\u0026quot;trycmd-example\u0026quot;). Lastly, we run all the test cases in the tests/cmd directory by invoking t.case(\u0026quot;tests/cmd/*.toml\u0026quot;).\nRun the test case.\ncargo test The test case will fail because we don\u0026rsquo;t have right output in the tests/cmd/help.toml file.\nrunning 1 test Testing tests/cmd/help.toml ... failed Exit: success ---- expected: stdout ++++ actual: stdout 1 + Simple program to greet a person 2 + 3 + Usage: trycmd-example [OPTIONS] --name \u0026lt;NAME\u0026gt; 4 + 5 + Options: 6 + -n, --name \u0026lt;NAME\u0026gt; Name of the person to greet 7 + -c, --count \u0026lt;COUNT\u0026gt; Number of times to greet [default: 1] 8 + -h, --help Print help 9 + -V, --version Print version stderr: Update snapshots with `TRYCMD=overwrite` Debug output with `TRYCMD=dump` test test_cmd ... FAILED Overwrite the output AS you can see from the output, we can use TRYCMD=overwrite to overwrite the output in the tests/cmd/help.toml file.\nTRYCMD=overwrite cargo test Note: If you are using Windows, your test output will be different from the output above. This is because on Windows the executable file extension is .exe. So the output would be trycmd-example.exe instead of trycmd-example. So you can set it totrycmd-example[EXE] in tests/cmd/help.toml to make it work on all platforms.\nAdd a Markdown test case Usually, we use this feature to test the README.md or other example files.\nCreate a README.md file.\ntouch README.md # trycmd-example ```console $ trycmd-example --help ``` Add README.md as a test case.\n// tests/cmd.rs #[test] fn test_cmd() { let t = trycmd::TestCases::new(); let trycmd_example_binary = trycmd::cargo::cargo_bin(\u0026#34;trycmd-example\u0026#34;); t.register_bin(\u0026#34;trycmd-example\u0026#34;, \u0026amp;trycmd_example_binary); t.case(\u0026#34;tests/cmd/*.toml\u0026#34;); t.case(\u0026#34;README.md\u0026#34;); // \u0026lt;-- Add this line } Run the test case.\ncargo test The test case will fail because we don\u0026rsquo;t have right output in the README.md file.\nrunning 1 test Testing tests/cmd/help.toml ... ok Testing README.md:4 ... failed Exit: success ---- expected: stdout ++++ actual: stdout 1 + Simple program to greet a person 2 + 3 + Usage: trycmd-example [OPTIONS] --name \u0026lt;NAME\u0026gt; 4 + 5 + Options: 6 + -n, --name \u0026lt;NAME\u0026gt; Name of the person to greet 7 + -c, --count \u0026lt;COUNT\u0026gt; Number of times to greet [default: 1] 8 + -h, --help Print help 9 + -V, --version Print version stderr: Update snapshots with `TRYCMD=overwrite` Debug output with `TRYCMD=dump` test test_cmd ... FAILED Overwrite the output.\nTRYCMD=overwrite cargo test trycmd will overwrite the output in the README.md file.\nUse directory to store input and output files To organize input and output files effectively, we can utilize a dedicated directory specifically designed for storing these files.\nRight now, we print the output to the console. If we want to print the output to a file, we also can test it with trycmd.\nChange the main.rs file.\n// src/main.rs use std::io::Write; // \u0026lt;-- Add this line use clap::Parser; /// Simple program to greet a person #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { /// Name of the person to greet #[arg(short, long)] name: String, /// Number of times to greet #[arg(short, long, default_value_t = 1)] count: u8, } fn main() { let args = Args::parse(); // Print the greeting to a file. \u0026lt;-- Add this line let mut file = std::fs::File::create(\u0026#34;greeting.txt\u0026#34;).unwrap(); // \u0026lt;-- Add this line for _ in 0..args.count { // \u0026lt;-- Add this line writeln!(file, \u0026#34;Hello, {}!\u0026#34;, args.name).unwrap(); // \u0026lt;-- Add this line } // \u0026lt;-- Add this line } Add a new TOML test case.\ntouch tests/cmd/greeting.toml # tests/cmd/greeting.toml bin.name = \u0026#34;trycmd-example\u0026#34; args = [\u0026#34;--name\u0026#34;, \u0026#34;foo\u0026#34;] status.code = 0 stdout = \u0026#34;\u0026#34; stderr = \u0026#34;\u0026#34; Add an output directory and an output file.\nmkdir tests/cmd/greeting.out touch tests/cmd/greeting.out/greeting.txt Run the test case.\ncargo test The test case will fail because we don\u0026rsquo;t have right output in the tests/cmd/greeting.out/greeting.txt file.\nrunning 1 test Testing README.md:4 ... ok Testing tests/cmd/help.toml ... ok Testing tests/cmd/greeting.toml ... ok Testing tests/cmd/greeting.toml:teardown ... failed Failed: Files left in unexpected state tests/cmd/greeting.out: is good ---- expected: tests/cmd/greeting.out/greeting.txt ++++ actual: /private/var/folders/76/zkdsk83x0dl3qydmhxf9dj3h0000gn/T/.tmpW1Aqys/greeting.txt 1 + Hello, foo! Update snapshots with `TRYCMD=overwrite` Debug output with `TRYCMD=dump` test test_cmd ... FAILED Overwrite the output.\nTRYCMD=overwrite cargo test trycmd will overwrite the output in the tests/cmd/greeting.out/greeting.txt file.\nSummary In this tutorial, we learned how to use trycmd to test a CLI program. We also learned how to use TRYCMD=overwrite to overwrite the output in the test case files. This feature is very useful when we want to update the output in the test case files. Hope you enjoy this tutorial and use trycmd to test your CLI programs.\nYou can find the source code of this tutorial in this repository.\nReference trycmd clap rustup ","permalink":"https://0xpoe.dev/posts/2023-07-03-use-trycmd-to-test-your-rust-cli/","summary":"\u003cp\u003eRecently we released \u003ca href=\"https://blog.rust-lang.org/2023/04/25/Rustup-1.26.0.html\"\u003eRustup 1.26.0\u003c/a\u003e, which includes a bunch of new features and bug fixes. We also upgraded the \u003ca href=\"https://github.com/clap-rs/clap\"\u003eclap\u003c/a\u003e version to 3.2.25, which is a major version upgrade. This upgrade was a bit tricky, because clap 3.0.0 had a lot of breaking changes. We needed to make sure that the new version of Rustup worked as expected. So before we upgraded clap we added \u003ca href=\"https://github.com/rust-lang/rustup/blob/master/tests/suite/cli_ui.rs\"\u003eUI tests\u003c/a\u003e for Rustup. We use \u003ca href=\"https://github.com/assert-rs/trycmd\"\u003etrycmd\u003c/a\u003e to test the CLI of Rustup. In this post I will show you how to use \u003ccode\u003etrycmd\u003c/code\u003e to test your CLI.\u003c/p\u003e","title":"How to use trycmd to test your Rust CLI?"},{"content":" ","permalink":"https://0xpoe.dev/presentations/2023-06-07-new-sink/","summary":"\u003cdiv style=\"position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;\"\u003e\n      \u003ciframe allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen\" loading=\"eager\" referrerpolicy=\"strict-origin-when-cross-origin\" src=\"https://www.youtube.com/embed/TleIDFCgvjg?autoplay=0\u0026amp;controls=1\u0026amp;end=0\u0026amp;loop=0\u0026amp;mute=0\u0026amp;start=0\" style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;\" title=\"YouTube video\"\u003e\u003c/iframe\u003e\n    \u003c/div\u003e\n\n\u003chr\u003e\n\u003ciframe src=\"https://ticdc-new-sink.slides.0xpoe.dev/\" width=\"100%\" height=\"500px\" frameborder=\"0\" allowfullscreen\u003e\u003c/iframe\u003e","title":"TiCDC New Sink Component"},{"content":"我近半年的时间都在做 TiCDC Sink 模块的改造工作，目前新的 Sink 实现已经成功替换了旧的实现。最近有客户希望通过自己实现 Sink 的方式来接入 TiCDC，所以我想把这段时间的改造和设计经验分享出来，希望能帮助到大家。\n此博客在 GitHub 上公开发布。 如果您有任何问题，请在此处打开一个 issue。\n⚠️ 注意：\n该指南主要面向开发者，如果您只是想使用 TiCDC，请参阅 TiCDC 使用文档。 在阅读该指南前，请先阅读 TiCDC 架构和数据同步链路解析了解 TiCDC 的基本架构和数据同步流程。 基本概念 可以先简单浏览这些子组件概念，后面会有详细的介绍。\nSink：TiCDC 的 Sink 模块负责将 TiCDC 的数据变更输出到外部系统中。目前 TiCDC 支持输出到 MySQL、TiDB、Kafka、S3 等外部系统中。 Table Sink：负责将 TiCDC 的数据变更按照表为单位进行聚合，然后输出到外部系统中。 Event Sink：负责与外部系统进行交互，将 TiCDC 的数据变更编码后输出到外部系统中。这里的 Event 主要指的是 TiCDC 的数据变更事件，比如 Insert、Update、Delete 等。 MQ Event Sink：负责将 TiCDC 的数据变更输出到 Message Queue 中。MQ Sink 会将数据变更事件编码为 MQ 消息，然后输出到 MQ 中。目前 TiCDC 支持输出到 Kafka 中。 Txn Event Sink：负责将 TiCDC 的数据变更按照事务为单位进行聚合，然后输出到外部系统中。目前 TiCDC 支持输出到 MySQL、TiDB 中。 DDL Sink：负责将 TiCDC 的 DDL 语句输出到外部系统中。目前 TiCDC 支持输出到 Kafka、MySQL、TiDB 中。 MQ DDL Sink：负责将 TiCDC 接受到的 DDL 语句输出到 Kafka 中。 Txn DDL Sink：，负责将 TiCDC 接受到的 DDL 语句输出到 MySQL、TiDB 中。 基本架构 我们可以将 TiCDC 接收到的数据分为两类：\nDML：TiCDC 接收到的数据变更事件，比如 Insert、Update、Delete 等。 DDL：TiCDC 接收到的 DDL 语句。 Sink 模块也根据上述不同的数据类型抽象出了不同的 Sink 子模块，分别是 Event Sink、DDL Sink。Event Sink 负责将过滤和聚合后的 DML 数据输出到外部系统中，DDL Sink 负责将过滤后的 DDL 数据输出到外部系统中。\nDDL Sink 很容易理解，因为它就是简单的将收到每张表的 DDL 语句编码后输出到外部系统。而 Event Sink 则更加复杂，我们会接收到大量不同表的变更数据，但是 TiCDC 需要按照表为单位进行数据同步。所以我们又引入了 Table Sink，它负责将收到的数据按照表进行聚合，然后输出到 Event Sink 中。\n我们可以将 TiCDC 的 Sink 模块抽象为下面这个图：\n有了这个基本的架构，我们就可以看看数据是如何在各个子模块之间流动的了。\n数据流程 数据同步流程也可以根据数据类型分为两部分，一部分是 DML 数据，另一部分是 DDL 数据。\nDML 数据 TiCDC 从 TiKV 接受到变更数据后，会对数据进行排序，但是整个排序过程中数据都是所有表的数据放在一起进行排序的。排序完成后我们还需要以表为单位将数据进行分发，这个过程就是 Table Sink 负责的。所以其他组件跟 Sink 模块的交互都是通过调用 Table Sink 的接口来完成的。\n这个过程中 Table Sink 可以理解成一个缓冲区，它会将收到的数据按照表进行缓存，但是并不会真实的将数据写入外部系统。与外部系统的交互是通过 Event Sink 来完成的，通过这样的抽象，多个 Table Sink 可以共享一个 Event Sink，我们可以在底层并发的进行数据写入。另外，我们也能共用一些公共的资源，比如数据库连接池，Kafka 的生产者等等。\n下面是 DML 数据在 MQ Event Sink 模块中流转的时序图：\n在上图中 Table Sink1 和 Table Sink2 通过调用 Event Sink 的 WriteEvents 将数据异步的写入到 Kafka 中，在等到写入的消息收到 ACK 之后，再通过调用每个事件的 Callback 来通知 Table Sink 数据已经写入成功。整个数据写入过程都是异步的，这样可以提高数据写入的吞吐量。\nDDL 数据 TiCDC 从 TiKV 接受到 DDL 变更数据后，会将数据直接发送到具体的 DDL Sink 实现中，这个过程是同步的，也就是说 TiCDC 会等待 DDL Sink 返回成功之后才会继续处理后续的 DDL 变更数据。在写入 DDL 的时候我们并没有使用 Table Sink，因为 DDL 数据是全局共用且有序的，所以我们可以直接将数据发送到 DDL Sink 中。\n下面是 DDL 数据在 MQ DDL Sink 模块中流转的时序图：\nTable Sink 在 Sink 模块中，Table Sink 是一个抽象的接口：\n// https://github.com/pingcap/tiflow/blob/master/cdc/sinkv2/tablesink/table_sink.go // 用于将数据以表为单位进行缓存。 type TableSink interface { // AppendRowChangedEvents 将行变更事件追加到 Table Sink 中。 // 注意：此方法不是线程安全的，请不要并发调用。 AppendRowChangedEvents(rows ...*model.RowChangedEvent) // UpdateResolvedTs 将聚合完成的数据发送到 Event Sink 中。 // 注意：此方法是异步的且不是线程安全的。 UpdateResolvedTs(resolvedTs model.ResolvedTs) error // GetCheckpointTs 返回 Table Sink 中的 CheckpointTs。 // 注意：此方法是线程安全的。 GetCheckpointTs() model.ResolvedTs // Close 关闭 Table Sink 并释放资源。 // 注意：我们需要保证这个方法是可取消或者可中断的。 Close(ctx context.Context) } 它最重要的两个方法是 AppendRowChangedEvents 和 UpdateResolvedTs，前者用于将行变更事件追加到 Table Sink 的缓存中，后者用于将聚合完的数据发送到 Event Sink 中。在 TiCDC 中，我们实现了一个基于内存的 Table Sink，它会将行变更事件聚合到内存中。\n因为针对的外部系统的不同，我们需要实现不同的聚合策略。所以我们为 Table Sink 的实现添加了一个范型参数 E，用于指定聚合策略：\n// https://github.com/pingcap/tiflow/blob/master/cdc/sinkv2/tablesink/table_sink.go type EventTableSink[E eventsink.TableEvent] struct { ... // 就是具体的 Event Sink，比如 MQ Event Sink。 backendSink eventsink.EventSink[E] ... // 用于实现不同的聚合策略。 eventAppender eventsink.Appender[E] // 注意：数据是按照 CommitTs 排序的。 eventBuffer []E ... } 可以看到范型参数 E 的类型为 eventsink.TableEvent，它是一个非常简单的接口：\n// https://github.com/pingcap/tiflow/blob/master/cdc/sinkv2/eventsink/event.go type TableEvent interface { // GetCommitTs 返回事件的 CommitTs。 GetCommitTs() uint64 } 通过这个接口抽象，任何可以获取 CommitTs 的事件都可以作为 Table Sink 的缓存对象。在 TiCDC 中，我们实现了两种聚合策略：\nRowChangedEvent：用于单行变更，比如 MQ Event Sink 就是将行变更一条一条发送到 Kafka 中。 SingleTableTxn：用于单表事务，比如 Txn Event Sink 就是以事务为单位提交到 MySQL 中。 还记得我们在数据流程中提到 DML 数据的写入是异步的吗？所以我们需要为每个 Event 添加一个 Callback，用于在数据写入完成后通知 Table Sink：\n// https://github.com/pingcap/tiflow/blob/master/cdc/sinkv2/eventsink/event.go type CallbackFunc func() type CallbackableEvent[E TableEvent] struct { Event E Callback CallbackFunc ... } // RowChangeCallbackableEvent 是行变更事件，它可以被回调。 type RowChangeCallbackableEvent = CallbackableEvent[*model.RowChangedEvent] // TxnCallbackableEvent 是单表事务事件，它可以被回调。 type TxnCallbackableEvent = CallbackableEvent[*model.SingleTableTxn] 有了这两种不同的可以回调的 Event，我们就可以实现具体的聚合策略了。为了复用代码，我们将聚合策略抽象为一个接口：\n// https://github.com/pingcap/tiflow/blob/master/cdc/sinkv2/eventsink/event_appender.go type Appender[E TableEvent] interface { // Append 添加一批行变更事件到缓存中。 Append(buffer []E, rows ...*model.RowChangedEvent) []E } 这样我们只需要实现 Append 方法，就可以实现不同的聚合策略了。\n对于 RowChangeCallbackableEvent 来说，我们并没有实际上的聚合操作，只是将行变更事件顺序追加到当前的缓存中：\n// https://github.com/pingcap/tiflow/blob/master/cdc/sinkv2/eventsink/event_appender.go func (r *RowChangeEventAppender) Append( buffer []*model.RowChangedEvent, rows ...*model.RowChangedEvent, ) []*model.RowChangedEvent { return append(buffer, rows...) } 对于 TxnCallbackableEvent 来说，我们需要将行变更事件聚合到一个事务中：\n// https://github.com/pingcap/tiflow/blob/master/cdc/sinkv2/eventsink/event_appender.go func (t *TxnEventAppender) Append( buffer []*model.SingleTableTxn, rows ...*model.RowChangedEvent, ) []*model.SingleTableTxn { for _, row := range rows { // 这意味我们目前还没有任何事务，所以我们需要创建一个新的事务。 if len(buffer) == 0 { txn := \u0026amp;model.SingleTableTxn{ StartTs: row.StartTs, CommitTs: row.CommitTs, Table: row.Table, TableInfo: row.TableInfo, } txn.Append(row) buffer = append(buffer, txn) continue } lastTxn := buffer[len(buffer)-1] lastCommitTs := lastTxn.GetCommitTs() ... // 使用 StartTs 来判断是否是同一个事务。如果不是同一个事务，我们需要创建一个新的事务。 if row.SplitTxn || lastTxn.StartTs != row.StartTs { buffer = append(buffer, \u0026amp;model.SingleTableTxn{ StartTs: row.StartTs, CommitTs: row.CommitTs, Table: row.Table, TableInfo: row.TableInfo, }) } buffer[len(buffer)-1].Append(row) } return buffer } 在上面的代码中，我们将 StartTs 作为界限，将行变更事件聚合到一个事务中。在 TiDB 中两个事务的 CommitTs 可能相同，但是 StartTs 一定不同，所以我们可以通过 StartTs 来判断行变更是否属于同一个事务。\n有了不同聚合策略的 Appender，我们就可以实现具体的 Table Sink 了。首先是 AppendRowChangedEvents 方法，得益于 Appender 的抽象，我们的具体实现可以非常简单：\n// https://github.com/pingcap/tiflow/blob/master/cdc/sinkv2/tablesink/table_sink.go func (e *EventTableSink[E]) AppendRowChangedEvents(rows ...*model.RowChangedEvent) { e.eventBuffer = e.eventAppender.Append(e.eventBuffer, rows...) } 其次是 UpdateResolvedTs 方法，它会将当前的缓存中的所有 Event 写入到具体的 Event Sink 中：\n// https://github.com/pingcap/tiflow/blob/master/cdc/sinkv2/tablesink/table_sink.go func (e *EventTableSink[E]) UpdateResolvedTs(resolvedTs model.ResolvedTs) error { ... // 从缓存中找到第一个大于 resolvedTs 的数据。 i := sort.Search(len(e.eventBuffer), func(i int) bool { return e.eventBuffer[i].GetCommitTs() \u0026gt; resolvedTs.Ts }) ... // 将该数据之前的所有数据取出来。 resolvedEvents := e.eventBuffer[:i] ... // 为每一个取出来的数据创建一个 CallbackableEvent。 resolvedCallbackableEvents := make([]*eventsink.CallbackableEvent[E], 0, len(resolvedEvents)) for _, ev := range resolvedEvents { ce := \u0026amp;eventsink.CallbackableEvent[E]{ Event: ev, Callback: e.progressTracker.addEvent(), SinkState: \u0026amp;e.state, } resolvedCallbackableEvents = append(resolvedCallbackableEvents, ce) } ... // 将取出来的数据写入到下游。 return e.backendSink.WriteEvents(resolvedCallbackableEvents...) } 可以看到在 UpdateResolvedTs 方法中，我们将缓存中的数据以 ResolvedTs 为界限取出来，然后将其转换为 CallbackableEvent，最后将其写入到具体的 backendSink 中。需要注意的是，我们能这样做的前提是数据在之前的模块中已经经过了排序，这样我们才能保证数据的顺序性。\n以上就是 Table Sink 的核心实现，只要实现不同的 Appender，我们就可以实现不同的 Table Sink。目前 TiCDC 主要支持的就是上述的两种聚合策略，你可以根据自己的需求来选择不同的聚合策略。一般来说，你不需要自己实现 Table Sink，目前的两种聚合策略已经能满足大部分的需求。\nEvent Sink 在 TiCDC 中，Event Sink 是真实与外部系统交互的模块，它的主要职责是将 Table Sink 中的数据写入到外部系统中。它的主要接口如下：\n// https://github.com/pingcap/tiflow/blob/master/cdc/sinkv2/eventsink/event_sink.go type EventSink[E TableEvent] interface { // WriteEvents 将数据写入到外部系统中。 // 注意：这是一个异步且线程安全的方法。 WriteEvents(events ...*CallbackableEvent[E]) error // Close 关闭 Event Sink。 // 注意：我们需要保证这个方法是可取消或者可中断的。 Close() error } 这个接口的实现是与具体的外部系统相关的，我们以 MQ Event Sink 为例来看一下它的实现：\n// https://github.com/pingcap/tiflow/blob/master/cdc/sinkv2/eventsink/mq/mq_dml_sink.go type dmlSink struct { ... worker *worker ... ctx context.Context cancel context.CancelFunc } 可以看到，它的实现中包含了一个 worker，这个 worker 用来处理写入到外部系统的数据。它的主要职责是将写入的数据进行编码，把 TiDB 的数据类型转化为 MQ 系统的数据类型，然后异步的将数据写入到 MQ 系统中。我们在这里就不展开讲解了，因为它的实现与具体的外部系统相关，你可以在 Sink 模块 中找到它的具体实现。\n我们来看一下 WriteEvents 方法的实现：\n// https://github.com/pingcap/tiflow/blob/master/cdc/sinkv2/eventsink/mq/mq_dml_sink.go func (s *dmlSink) WriteEvents(rows ...*eventsink.RowChangeCallbackableEvent) error { for _, row := range rows { ... // 将数据通过 channel 发送到 worker 中。 s.worker.msgChan.In() \u0026lt;- mqEvent{ ... rowEvent: row, } } return nil } 它的实现非常简单，就是将数据通过 channel 发送到 worker 中，然后 worker 封装了具体的写入逻辑，主要是将数据进行编码后调用外部系统的 API 进行写入。例如：将 TiDB 的数据编码为一条 Kafka 消息，然后调用 Kafka 生产者的 Produce 方法进行写入。\n我们可以注意到，这里的 rows 都是从 Table Sink 中下发的可以回调的数据，在 worker 写入成功后，我们就可以通过回调的方式来通知 Table Sink 数据已经写入成功。\nDDL Sink 在 TiCDC 中，DDL Event 比较特殊，因为 DDL 是全局共用的，所以我们只需要有一个 DDL Sink 按照顺序将 DDL 写入到外部系统中即可。它的主要接口如下：\n// https://github.com/pingcap/tiflow/blob/master/cdc/sinkv2/ddlsink/ddl_sink.go type DDLEventSink interface { // WriteDDLEvent 将 DDL 写入到外部系统中。 // 注意：这是一个同步且线程安全的方法。 WriteDDLEvent(ctx context.Context, ddl *model.DDLEvent) error // WriteCheckpointTs 将 CheckpointTs 写入到外部系统中。 // 注意：这是一个同步且线程安全的方法。 // 目前只有 MQ DDL Sink 会实现这个方法。 WriteCheckpointTs(ctx context.Context, ts uint64, tables []*model.TableInfo) error // Close 关闭 DDL Sink 和释放相关资源。 Close() error } 它的核心方法是 WriteDDLEvent，我们将 DDL Event 传入之后，它会将 DDL Event 进行编码，然后写入到外部系统中。例如：将 TiDB 的 DDL Event 编码为一条 Kafka 消息，然后调用 Kafka 生产者的 Produce 方法进行写入。注意，这个方法的实现需要是同步的，因为 DDL 数量不会很多，所以我们可以将它的实现设置为同步的，这样可以避免一些并发问题，让实现变得简单。\n值得一提的是，MQ DDL Sink 会实现 WriteCheckpointTs 方法，它会将 CheckpointTs 写入到 MQ 系统中。它会作为一个特殊的消息告诉下游系统当前的 CheckpointTs，我们可以把它理解成 MQ 系统中的 Watermark。这样，我们在消费 MQ 系统中的消息时，就可以通过 CheckpointTs 来进行过滤，排序等操作。\nLog Sink 示例 介绍了上面的各种 Sink 概念和实现之后，我们可以动手自己实现一个简单的 Log Sink 来了解如何为 TiCDC 添加一个新的 Sink。我们可以假设 Log Sink 的主要职责就是将所有的数据输出到日志中。我们调用 log.Info() 写入数据即可。\n首先，我们需要确认聚合策略，根据不同的聚合策略我们可以选用不同的 Table Sink 实现。因为我们的 Log Sink 只是简单的将数据写入到日志中，所以我们可以选择 RowChangeEventAppender 作为我们的 Appender。这样每一行数据都会被写入到日志中。\n接下来，我们只需要实现一个具体的 Event Sink 和一个 DDL Sink 即可。首先是 Event Sink：\n// 断言 LogSink 实现了 EventSink 接口。 var _ eventsink.EventSink[*model.RowChangedEvent] = (*LogSink)(nil) type LogSink struct{} // New 创建一个新的 LogSink。 func New() *LogSink { return \u0026amp;LogSink{} } func (s *LogSink) WriteEvents(rows ...*eventsink.CallbackableEvent[*model.RowChangedEvent]) error { for _, row := range rows { log.Info(\u0026#34;LogSink: WriteEvents\u0026#34;, zap.Any(\u0026#34;row\u0026#34;, row.Event)) // 不要忘记调用 Callback 方法来通知 Table Sink 该 Event 已经被处理。 row.Callback() } return nil } func (s *LogSink) Close() error { return nil } 我们在实现 WriteEvents 方法时，只需要将每一行数据写入到日志中即可。在 此外，我们还需要调用 Callback 方法来通知 Table Sink 该 Event 已经被处理。注意，所有的 Callback 方法是必须调用的，否则 Table Sink 会一直等待该 Event 被处理，这样会导致 TiCDC 数据同步卡住。\n另外，我们可以注意到我在第一行代码中做了一个断言，这是为了确保我们的 Log Sink 实现了 EventSink 接口。这样，我们在编译时就可以发现一些错误。建议大家在实现 Sink 时都做这样的断言。\n接下来是 DDL Sink：\n// 断言 LogSink 实现了 DDLEventSink 接口。 var _ ddlsink.DDLEventSink = (*LogSink)(nil) type LogSink struct{} // New create a black hole DDL sink. func New() *LogSink { return \u0026amp;LogSink{} } func (d *LogSink) WriteDDLEvent(ctx context.Context, ddl *model.DDLEvent, ) error { log.Info(\u0026#34;LogSink: DDL Event\u0026#34;, zap.Any(\u0026#34;ddl\u0026#34;, ddl)) return nil } func (d *LogSink) WriteCheckpointTs(ctx context.Context, ts uint64, tables []*model.TableInfo, ) error { log.Info(\u0026#34;LogSink: Checkpoint Ts Event\u0026#34;, zap.Uint64(\u0026#34;ts\u0026#34;, ts), zap.Any(\u0026#34;tables\u0026#34;, tables)) return nil } // Close do nothing. func (d *LogSink) Close() error { return nil } DDL Sink 的实现非常简单，我们只需要将 DDL Event 写入到日志中即可。\n有了具体的 Event Sink 和 DDL Sink 之后，我们就可以尝试将其接入到 TiCDC 中了。在接入之前，我们需要了解一下 TiCDC 目前是如何构造 Sink 的。 因为 golang 范型的限制，我们无法直接将一个带范型的 Event Sink 传递给 Table Sink。所以我们构建了一个构造工厂来解决这个问题：\n// https://github.com/pingcap/tiflow/blob/master/cdc/sinkv2/eventsink/factory/factory.go type SinkFactory struct { sinkType sink.Type rowSink eventsink.EventSink[*model.RowChangedEvent] txnSink eventsink.EventSink[*model.SingleTableTxn] } // New 根据协议类型创建一个 SinkFactory。 func New(ctx context.Context, sinkURIStr string, cfg *config.ReplicaConfig, errCh chan error, ) (*SinkFactory, error) { sinkURI, err := config.GetSinkURIAndAdjustConfigWithSinkURI(sinkURIStr, cfg) if err != nil { return nil, err } s := \u0026amp;SinkFactory{} schema := strings.ToLower(sinkURI.Scheme) switch schema { case sink.MySQLScheme, sink.MySQLSSLScheme, sink.TiDBScheme, sink.TiDBSSLScheme: txnSink, err := txn.NewMySQLSink(ctx, sinkURI, cfg, errCh, txn.DefaultConflictDetectorSlots) if err != nil { return nil, err } s.txnSink = txnSink s.sinkType = sink.TxnSink case sink.KafkaScheme, sink.KafkaSSLScheme: mqs, err := mq.NewKafkaDMLSink(ctx, sinkURI, cfg, errCh, kafka.NewSaramaAdminClient, kafka.NewSaramaClient, dmlproducer.NewKafkaDMLProducer) if err != nil { return nil, err } s.rowSink = mqs s.sinkType = sink.RowSink ... default: return nil, cerror.ErrSinkURIInvalid.GenWithStack(\u0026#34;the sink scheme (%s) is not supported\u0026#34;, schema) } return s, nil } // CreateTableSink 创建一个 Table Sink，并且传入 Event Sink。 func (s *SinkFactory) CreateTableSink( changefeedID model.ChangeFeedID, span tablepb.Span, totalRowsCounter prometheus.Counter, ) tablesink.TableSink { switch s.sinkType { case sink.RowSink: // 我们需要在这里显式地指定 Event Sink 的类型。否则 golang 无法推断出该类型。 return tablesink.New[*model.RowChangedEvent](changefeedID, span, s.rowSink, \u0026amp;eventsink.RowChangeEventAppender{}, totalRowsCounter) case sink.TxnSink: return tablesink.New[*model.SingleTableTxn](changefeedID, span, s.txnSink, \u0026amp;eventsink.TxnEventAppender{}, totalRowsCounter) default: panic(\u0026#34;unknown sink type\u0026#34;) } } 首先，我们通过 New 函数根据协议类型创建一个 SinkFactory。这个过程中我们会创建一个 Event Sink 的具体实例。比如我们新增了 Log Sink，那么我们就可以在这里创建一个 Log Sink 的实例：\n// https://github.com/pingcap/tiflow/blob/master/cdc/sinkv2/eventsink/factory/factory.go func New(ctx context.Context, sinkURIStr string, cfg *config.ReplicaConfig, errCh chan error, ) (*SinkFactory, error) { sinkURI, err := config.GetSinkURIAndAdjustConfigWithSinkURI(sinkURIStr, cfg) if err != nil { return nil, err } s := \u0026amp;SinkFactory{} schema := strings.ToLower(sinkURI.Scheme) switch schema { ... ... +\tcase sink.LogScheme: +\ts.rowSink = logsink.New() +\t// 根据我们上面选择的聚合策略，我们选择 RowSink。 +\ts.sinkType = sink.RowSink default: return nil, cerror.ErrSinkURIInvalid.GenWithStack(\u0026#34;the sink scheme (%s) is not supported\u0026#34;, schema) } return s, nil } 在这里我们新增了一个 LogScheme，假设它为 log://，那么当用户传入 --sink-uri=\u0026quot;log://\u0026quot; 时，我们就会创建一个 Log Sink 的实例。在创建过程中我们使用了 sink.RowSink 作为 Table Sink 的类型，这是因为我们选择了以行为单位的聚合策略。\n最后，我们需要关注一下 CreateTableSink 函数，这个函数会创建一个 Table Sink，并且传入 Event Sink。可以看到我们在这里根据 sinkType 指定的范型参数为 *model.RowChangedEvent 这样就实现了 Table Sink 和 Event Sink 的组合。\n这样我们就完整地实现了一个新的 Sink。接下来只需要指定 --sink-uri=\u0026quot;log://\u0026quot; 即可使用我们的 Log Sink。\n总结 本文我介绍了 TiCDC 的 Sink 模块，我们了解了 Sink 详细设计，以及如何实现一个新的 Sink。但是要实现一个可用的 Sink，我们还需要考虑更多的问题，比如：\n数据写入的效率 数据编码效率 数据写入失败的处理 参数和配置的处理 单元测试的编写 集成测试的编写 使用文档的编写 模块的可观测性 输出的数据如何被消费和使用 输出的数据的正确性校验 等等。**所以如果你真的希望自己动手实现一个生产级别的 Sink，那么你可以参考我们目前已经 GA 的 Sink 实现来实现你自己的 Sink。**如果你在实现过程中遇到了问题，欢迎在 TiCDC 的 Issue Tracker 中提出问题我们一起讨论和解决问题。\n","permalink":"https://0xpoe.dev/posts/2023-02-07-ticdc-sink-%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/","summary":"\u003cp\u003e我近半年的时间都在做 \u003ca href=\"https://docs.pingcap.com/tidb/dev/ticdc-overview/\"\u003eTiCDC\u003c/a\u003e \u003ca href=\"https://github.com/pingcap/tiflow/tree/master/cdc/sinkv2\"\u003eSink 模块\u003c/a\u003e的改造工作，目前新的 Sink 实现已经成功替换了旧的实现。最近有客户希望通过自己实现 Sink 的方式来接入 TiCDC，所以我想把这段时间的改造和设计经验分享出来，希望能帮助到大家。\u003c/p\u003e\n\u003cp\u003e此博客在 \u003ca href=\"https://github.com/0xPoe/0xpoe.dev\"\u003eGitHub\u003c/a\u003e 上公开发布。 如果您有任何问题，请在此处打开一个 \u003ca href=\"https://github.com/0xPoe/0xpoe.dev/issues\"\u003eissue\u003c/a\u003e。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e⚠️ 注意：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e该指南主要面向开发者，如果您只是想使用 TiCDC，请参阅 \u003ca href=\"https://docs.pingcap.com/zh/tidb/stable/ticdc-overview\"\u003eTiCDC 使用文档\u003c/a\u003e。\u003c/li\u003e\n\u003cli\u003e在阅读该指南前，请先阅读 \u003ca href=\"https://0xpoe.dev/TiCDC-%E6%9E%B6%E6%9E%84%E5%92%8C%E6%95%B0%E6%8D%AE%E5%90%8C%E6%AD%A5%E9%93%BE%E8%B7%AF%E8%A7%A3%E6%9E%90/\"\u003eTiCDC 架构和数据同步链路解析\u003c/a\u003e了解 TiCDC 的基本架构和数据同步流程。\u003c/li\u003e\n\u003c/ol\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"基本概念\"\u003e基本概念\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e可以先简单浏览这些子组件概念，后面会有详细的介绍。\u003c/p\u003e\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003eSink：TiCDC 的 Sink 模块负责将 TiCDC 的数据变更输出到外部系统中。目前 TiCDC 支持输出到 MySQL、TiDB、Kafka、S3 等外部系统中。\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/pingcap/tiflow/tree/master/cdc/sinkv2/tablesink\"\u003eTable Sink\u003c/a\u003e：负责将 TiCDC 的数据变更按照表为单位进行聚合，然后输出到外部系统中。\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/pingcap/tiflow/tree/master/cdc/sinkv2/eventsink\"\u003eEvent Sink\u003c/a\u003e：负责与外部系统进行交互，将 TiCDC 的数据变更编码后输出到外部系统中。这里的 Event 主要指的是 TiCDC 的数据变更事件，比如 Insert、Update、Delete 等。\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/pingcap/tiflow/tree/master/cdc/sinkv2/eventsink/mq\"\u003eMQ Event Sink\u003c/a\u003e：负责将 TiCDC 的数据变更输出到 Message Queue 中。MQ Sink 会将数据变更事件编码为 MQ 消息，然后输出到 MQ 中。目前 TiCDC 支持输出到 Kafka 中。\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/pingcap/tiflow/tree/master/cdc/sinkv2/eventsink/txn\"\u003eTxn Event Sink\u003c/a\u003e：负责将 TiCDC 的数据变更按照事务为单位进行聚合，然后输出到外部系统中。目前 TiCDC 支持输出到 MySQL、TiDB 中。\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/pingcap/tiflow/tree/master/cdc/sinkv2/ddlsink\"\u003eDDL Sink\u003c/a\u003e：负责将 TiCDC 的 DDL 语句输出到外部系统中。目前 TiCDC 支持输出到 Kafka、MySQL、TiDB 中。\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/pingcap/tiflow/tree/master/cdc/sinkv2/ddlsink/mq\"\u003eMQ DDL Sink\u003c/a\u003e：负责将 TiCDC 接受到的 DDL 语句输出到 Kafka 中。\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/pingcap/tiflow/tree/master/cdc/sinkv2/ddlsink/mysql\"\u003eTxn DDL Sink\u003c/a\u003e：，负责将 TiCDC 接受到的 DDL 语句输出到 MySQL、TiDB 中。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"基本架构\"\u003e基本架构\u003c/h2\u003e\n\u003cp\u003e我们可以将 TiCDC 接收到的数据分为两类：\u003c/p\u003e","title":"TiCDC Sink 开发指南"},{"content":" ","permalink":"https://0xpoe.dev/presentations/2022-11-05-ticdc/","summary":"\u003cdiv style=\"position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;\"\u003e\n      \u003ciframe allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen\" loading=\"eager\" referrerpolicy=\"strict-origin-when-cross-origin\" src=\"https://www.youtube.com/embed/n-Mete4KfAc?autoplay=0\u0026amp;controls=1\u0026amp;end=0\u0026amp;loop=0\u0026amp;mute=0\u0026amp;start=0\" style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;\" title=\"YouTube video\"\u003e\u003c/iframe\u003e\n    \u003c/div\u003e\n\n\u003chr\u003e\n\u003ciframe src=\"https://ticdc.slides.0xpoe.dev/\" width=\"100%\" height=\"500px\" frameborder=\"0\" allowfullscreen\u003e\u003c/iframe\u003e","title":"TiCDC, a tool for replicating the incremental data of TiDB"},{"content":"TiCDC 是 TiDB 生态中的一个数据同步工具，它通过拉取 TiKV 的变更日志实现数据的增量同步。它除了可以将 TiDB 的数据同步至 MySQL 兼容的数据库之外，还提供了同步至 Kafka 的能力，支持 canal 和 avro 多种开放消息协议供其他系统订阅数据变更。\n基本概念 以下这些都是理解 TiCDC 架构之前需要熟悉的基本概念，主要是对 TiDB 内部的一些组件和概念的解释。\nTiDB: 下文中提到的 TiDB 主要是指它作为是一个计算层的抽象提供执行 SQL 的能力，没有真实的存储数据。 TiKV: 一个支持 ACID 的键值数据库，它可以作为 TiDB 的存储层。 Region: 它是 TiKV 数据移动的基本单位，可以将它认为是一组键值对集合。每个 Region 被复制到多个 TiKV 节点。 两阶段提交：一种分布式算法，它协调所有参与分布式原子事务的进程，决定是否提交或中止（回滚）该事务。 StartTs: TiDB 在开始两阶段提交时获取到的一个全局唯一递增的时间戳作为当前事务的唯一事务 ID，这里称为该事务的 start_ts。 CommitTs: TiDB 在提交两阶段提交事务时获取到另外一个全局唯一递增的时间戳作为该事务的 commit_ts。 什么是 CDC？ CDC 的全称为 Change Data Capture，它是指从源数据库捕获数据并且将其同步到其他数据库或者应用程序的过程。它作为一种很常见的数据集成方式被大量的应用在数据仓库中。当然任何的数据库系统都可以构建自己的 CDC，比如 SQL Server 的 CDC。TiCDC 就是专属于 TiDB 的 CDC，它的上游只能是 TiDB，但是它的下游可以是其他 MySQL 兼容的数据库系统，也可以是消息队列。\n通过 TiCDC 我们可以实现 TiDB 集群之间的灾备和数据同步，也可以将 TiDB 的数据集成进其他数据处理系统。\nTiCDC 的架构 我们知道了 CDC 需要获取变更并将它同步给下游的系统，那对于 TiCDC 来说它就需要从 TiKV 拉取变更，因为 TiDB 集群写入的每一条数据最终都会被持久化到 TiKV 上。下面我们就从架构上来看一看 TiCDC 如何将数据从 TiKV 拉取并同步到下游系统中。\n我们将一个 TiCDC 节点称为一个 Capture，一个 Capture 的可能由下面两个组件组成：\nOwner: 一个 TiCDC 集群中的某个节点会被选举成为 Owner，它会负责处理任务的调度和 DDL 事件的处理。 Processor: 其他非 Owner 节点则会启动 Processor 进程来处理 Owner 调度过来的同步任务，它主要负责处理 DML 事件。 注意：Owner 节点也会启动 Processor 进程来处理同步任务，但是整个集群中有且仅有一个 Owner。\n另外我们注意到，在 TiKV 系统中也存在一个叫做 TiKV CDC 的组件，它就是数据同步的起点，所有的行变更都是由该组件通过 gRPC Stream 推送给 TiCDC 节点。\n我们先来看一看 Owner 组件的主要职责：\nOwner 会启动 TiCDC 中的 DDL Puller 从 TiKV 拉取 DDL 的变更，并且对收到的变更数据进行编解码，将其转化为 DDL SQL 语句然后通过 DDLSink 写入下游系统。 Owner 会通过 scheduler 组件向其他节点发送命令进行同步任务调度，让其启动 Processor 进程开始同步数据。 在 TiCDC 中我们把这种任务称为 Changefeed。每个 Changefeed 可能会根据配置同步多张表，Owner 会根据每个节点负责同步的表数量将一个 Changefeed 中的表平均的分配到多个节点。 Owner 会负责收集各个节点的同步进度，计算和统计全局的同步进度。 当其他节点收到来自 Owner 的同步命令之后，它们就会启动上图所示的 Processor 进程：\n每个 Processor 会负责同步一个任务。 当节点收到来自 Owner 的命令之后，会启动 Processor 进程，每个 Processor 会根据收到的任务详情启动 Table Pipeline，它作为一个流水线会负责以表为单位从 TiKV 拉取数据、排序数据、组装数据和写入数据到下游。 根据上述的架构我们知道 TiCDC 同步数据的核心流程是 Table Pipeline，那我们就来看一看一条 DML 被执行之后，如何从 TiKV 被捕获并同步至下游。\n数据同步链路 我们可以把 Table Pipeline 细化成四个部分：\nPuller: 负责与 TiKV CDC 组件建立 gRPC 连接并捕获数据 Sorter: 负责对拉取到的乱序数据进行排序，让其以表为单位按照事务提交时间进行排序 Mounter: 根据事务提交时的表结构信息解析和填充行变更，将行变更转化为 TiCDC 能直接处理的数据结构 Sink: 将 Mounter 处理过后的数据进行编解码，转化为 SQL 语句或者 Kafka 消息写入下游 一个例子 假设我们现在建立如下表结构：\nCREATE TABLE TEST( NAME VARCHAR (20) NOT NULL, AGE INT NOT NULL, PRIMARY KEY (NAME) ); +-------+-------------+------+------+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-------+-------------+------+------+---------+-------+ | NAME | varchar(20) | NO | PRI | NULL | | | AGE | int(11) | NO | | NULL | | +-------+-------------+------+------+---------+-------+ 此时我们在 TiDB 先后执行这两条 DML：\nINSERT INTO TEST (NAME,AGE) VALUES (\u0026#39;Jack\u0026#39;,20); UPDATE TEST SET AGE = 25 WHERE NAME = \u0026#39;Jack\u0026#39;; 下面我们就来看一看这两条 DML 会通过什么样的链路写入下游。\n数据写入到 TiKV 在 TiDB 执行 SQL 之后数据最终是以 key-value 的形式写入了 TiKV，我们可以暂时忽略掉 TiKV 具体如何写入数据的细节，从 key-value 的形式来看一看最终写入到 TiKV 的数据。\n执行第一条插入语句 INSERT INTO TEST (NAME,AGE) VALUES (\u0026#39;Jack\u0026#39;,20); +------------+-----------------+ | Key | Value | +------------+-----------------+ | TEST_Jack | Jack | 20 | +------------+-----------------+ 执行第二条更新语句 UPDATE TEST SET AGE = 25 WHERE NAME = \u0026#39;Jack\u0026#39;; +------------+-----------------+ | Key | Value | +------------+-----------------+ | TEST_Jack | Jack | 25 | +------------+-----------------+ 我们在示例中为了方便理解，将 key 简化为表名_主键的形式，但是请注意 TiKV 并不会真的按照这种格式存储数据。它实际上会为每个表分配 TableID、RowID 和 IndexID 来组成 key。\n当这些数据按照 key-value 的形式写入到 TiKV 之后，TiCDC 就可以与 TiKV 建立 gRPC 连接然后进行数据拉取。\nPuller 从 TiKV 拉取 我们在架构解析中提到 Puller 负责与 TiKV CDC 组件建立 gPRC 连接然后拉取数据，那我们就先来看一看 gRPC 的数据结构和接口定义。\n完整 proto 定义，请参考该文件。\nTiCDC 与 TiKV 之间的数据交互只有一个接口:\nservice ChangeData { rpc EventFeed(stream ChangeDataRequest) returns(stream ChangeDataEvent); } TiCDC 发送一个请求，然后与 TiKV 建立一个 gRPC Stream，源源不断的接收推送过来的数据。\nTiCDC 发送的请求信息:\nmessage ChangeDataRequest { uint64 region_id = 2; metapb.RegionEpoch region_epoch = 3; bytes start_key = 5; bytes end_key = 6; ... } 在请求信息中我们省略了很多无关信息，TiCDC 在与 TiKV 建立连接时主要需要关注的是：\nRegion 信息，因为在 TiKV 中，数据都被分散存储在每个 Region 及其副本上，所以从 TiKV 的角度看 TiCDC 拉取数据的单位是 Region。 start_key 和 end_key，因为从 TiCDC 的角度看，TiCDC 拉取数据的单位是 Table，所以当一个 Region 出现多个表数据时，TiCDC 可以通过 start_key 和 end_key 来指定拉取的范围。 TiKV 通过请求信息扫描出数据之后，会将结果作为一个 ChangeDataEvent 返回给 TiCDC：\nmessage Event { enum LogType { UNKNOWN = 0; PREWRITE = 1; COMMIT = 2; ROLLBACK = 3; COMMITTED = 4; ... } message Row { uint64 start_ts = 1; uint64 commit_ts = 2; LogType type = 3; enum OpType { UNKNOWN = 0; PUT = 1; DELETE = 2; } OpType op_type = 4; bytes key = 5; bytes value = 6; bytes old_value = 7; ... } } message ResolvedTs { repeated uint64 regions = 1; uint64 ts = 2; } message ChangeDataEvent { repeated Event events = 1; ResolvedTs resolved_ts = 2; } 在返回结果中，我们最需要关注的是 Row。我们在上面写入的数据就会被转换成：\n+-------------+--------------+------------+---------+--------------+------------------+------------------+ | start_ts | commit_ts | type | op_type | key | value | old_value | +-------------+--------------+------------+---------+--------------+------------------+------------------+ | 1 | 2 | COMMITTED | PUT | TEST_Jack | Jack | 20 | null | | 3 | 4 | COMMITTED | PUT | TEST_Jack | Jack | 25 | Jack | 20 | +-------------+--------------+------------+---------+--------------+------------------+------------------+ 我们可以看到 Insert 语句扫描出的数据只有 value 没有 old_value，而 Update 语句则被转化为一条既有 value 又有 old_value 的行变更数据。\n除了数据之外，我们可以看到还有一种叫做 ResolvedTs 的事件，这是一个在 TiCDC 系统中很重要的标识时间点，可以看到我们收到 Row 事件中都带有 commt_ts 这样的时间戳，而 ResolvedTs 事件的下发就意味着小于等于这个时间点提交的数据已经全部下发给 TiCDC，并且以后不会再有早于这个时间点的数据发送至 TiCDC，所以 TiCDC 可以以此为界限来尝试将收到的数据同步至下游。\n这样这两条数据就成功的被 Puller 拉取到了 TiCDC，但是因为 TiDB 中一张表的数据会被分散到多个 Region 上，所以 Puller 会与多个 TiKV Region Leader 节点建立连接，然后拉取数据。那实际上 TiCDC 拉取到的变更数据可能是乱序的，我们需要对拉取到的所有数据进行排序才能正确的将事务按照顺序同步到下游。\nSorter 进行排序 假设我们现在除了上述的两条数据之外，在该表上又进行了其他的写入操作，并且该操作的数据在另外一个 Region。最终 Puller 拉到的数据如下：\n+--------------------------------------------+-----------------------------------------------------+ | Region1 | Region2 | +--------------------------------------------+-----------------------------------------------------+ | | ts3: Test_Mick -\u0026gt; Mick | 18 | | ts2: TEST_Jack -\u0026gt; Jack | 20 | | | ts2: Resolved | | | ts3: TEST_Jack -\u0026gt; Jack | 25 | ts3: Resolved | | ts3: Resolved | | +--------------------------------------------+-----------------------------------------------------+ 我们可以看到拉取到的数据并不是按照 commit_ts 严格排序的，我们需要根据 commit_ts 作为依据将它们进行排序，最终得到如下的数据：\n+--------------------------------------------+ | Events | +--------------------------------------------+ | ts2: TEST_Jack -\u0026gt; Jack | 20 | | ts2: Resolved | | ts3: TEST_Jack -\u0026gt; Jack | 25 | | ts3: Test_Mick -\u0026gt; Mick | 18 | | ts3: Resolved | +--------------------------------------------+ 这样严格按照 commit_ts 排好顺序的事件就可以接着往下游同步了，同时我们也将 ResolvedTs 事件穿插在排序好的数据中，这是因为它也需要作为一种特殊事件被写入到后置的组件中，它会作为一个标志事件被用于驱动后置组件的下发行为。例如：后置组件在收到 commit_ts 等于 2 的 ResolvedTs 事件之后就可以将之前收到的 commit_ts 小于等于 2 的 DML 事件写入下游并且等待执行完成。\n但是下发之前我们需要先对数据做一些转换，因为我们现在收到的是从 TiKV 中扫描出的 key-value，我们无法直接将它转化为 SQL 写入下游，它们实际上只是一堆 bytes 数据。\nMounter 进行解析 当我们拿到这些 bytes 数据之后，我们需要对它进行一些解析，将它还原成按照表结构组织的数据。我们在架构中可以注意 Processor 也会通过 DDLPuller 来拉取表信息，并且将这些信息汇总在一个叫做 SchemaStorage 的结构中。Mounter 会从该结构中找到某个行变更当时的表结构信息，然后将其从 key-value 转化为携带表信息的结构体。\ntype RowChangedEvent struct { StartTs uint64 CommitTs uint64 Table *TableName ColInfos []rowcodec.ColInfo Columns []*Column PreColumns []*Column IndexColumns [][]int ... } 可以看到，该结构体中还原出了所有的表和列信息，并且 Columns 和 PreColumns 就对应于 value 和 old_value。当 TiCDC 拿到这些信息之后我们就可以将数据继续下发至 Sink 组件，让其根据表信息和行变更数据去写下游数据库或者生产 Kafka 消息。\nSink 进行下发 当一条条 RowChangedEvent 被下发至 Sink 组件之后，我们就可以将其转化为 SQL 或者特定消息格式的 Kafka 消息。在架构中我们可以看到有两种 Sink，一种是接入在 Table Pipeline 中的 TableSink，另外一种是 Processor 级别共用的 ProcessorSink。它们在系统中有不同的作用：\nTableSink 作为一种 Table 级别的管理单位，缓存着要下发到 ProcessorSink 的数据，它的主要作用是方便 TiCDC 按照表为单位管理资源和进行调度 ProcessorSink 作为真实要与数据库或者 Kafka 建立连接的 Sink 负责 SQL/Kafka 消息的转换和同步 我们再来看一看 ProcessorSink 到底如何转换这些行变更：\n如果下游是数据库，ProcessorSink 会根据 RowChangedEvent 中的 Columns 和 PreColumns 来判断它到底是一个 Insert、Update 还是 Delete 操作，然后根据不同的操作类型，将其转化为 SQL 语句，然后再将其通过数据库连接写入下游： /* 因为只有 Columns 所以是 Insert 语句。 */ INSERT INTO TEST (NAME,AGE) VALUES (\u0026#39;Jack\u0026#39;,20); /* 因为既有 Columns 且有 PreColumns 所以是 Update 语句。 */ UPDATE TEST SET AGE = 25 WHERE NAME = \u0026#39;Jack\u0026#39;; 如果下游是 Kafka, ProcessorSink 会作为一个 Kafka Producer 按照特定的消息格式将数据发送至 Kafka。 以 Canal-JSON 为例，我们上述的 Insert 语句最终会以如下的 JSON 格式写入 Kafka： { \u0026#34;id\u0026#34;: 0, \u0026#34;database\u0026#34;: \u0026#34;test\u0026#34;, \u0026#34;table\u0026#34;: \u0026#34;TEST\u0026#34;, \u0026#34;pkNames\u0026#34;: [ \u0026#34;NAME\u0026#34; ], \u0026#34;isDdl\u0026#34;: false, \u0026#34;type\u0026#34;: \u0026#34;INSERT\u0026#34;, ... \u0026#34;ts\u0026#34;: 2, \u0026#34;sql\u0026#34;: \u0026#34;\u0026#34;, ... \u0026#34;data\u0026#34;: [ { \u0026#34;NAME\u0026#34;: \u0026#34;Jack\u0026#34;, \u0026#34;AGE\u0026#34;: \u0026#34;25\u0026#34; } ], \u0026#34;old\u0026#34;: null } 这样一条条 DML 就会通过 Sink 源源不断的写入到下游了。虽然我们的数据源源不断的往下同步了，但是对于用户来说应该如何确定同步的进度呢？\n如何监测数据同步进度？ 用户在使用 TiCDC 的时候会很关心数据的同步进度，因为 TiCDC 是一个增量同步工具，上游会有源源不断的写入，TiCDC 会不断的处理和同步数据。所以可以把它认为是一个流处理系统。在流处理系统中我们常常会引入 watermark 或者 checkpoint 的概念来监测数据同步的进度。\n想要理解这个 checkpoint/watermark 的概念，我们先要梳理一下在流处理系统中的两类时间：\n处理时间(Processing time)：处理时间是指执行相应操作的机器的系统时间。对于 TiCDC 来说，它在内部的每个组件中完成数据拉取、转换等操作的时间就可以认为是处理时间。\n事件时间(Event time)：事件时间是指该事件发生时的逻辑时间。对于 TiCDC 来说，它指的是某个行变更在事务中被提交时的 commit_ts。\n从定义上来看，对用户洞察系统同步进度的有效时间是事件时间，我们可以通过当前 TiCDC 写完的行变更的 commit_ts 来确定同步的进度或者延迟。这就是我们的 checkpoint。\nTiCDC 就是通过 CheckpointTs 来标识数据同步的进度，而它的反馈就是从 Sink 组件来的，因为 TiCDC 接收数据并处理完成的标识就是 Sink 组件将该 SQL/Kakfa 消息写入到了下游并且收到了回复，这样我们就认为这条数据已经同步完成了。\n我们通过统计每个节点上 Processor 中的 Sink 组件的进度，就能计算出整个任务当前的同步进度或延迟。\n总结 我们通过一个 Table Pipeline 走完了一条 DML 的完整同步链路。在上述的文章中我们主要讨论的是 DML 的同步链路。我们忽略了 TiCDC 中如何处理 DDL 同步、如何进行表调度、如何利用 PD 进行元信息管理和保证集群高可用等问题。希望下次我能够再深入到这些问题中，为大家分享我们的解决方案。\n参考链接 TiCDC Sink Component\nStreaming Systems\nTiKV 是如何存取数据的\nTiKV 源码解析系列文章（十三）MVCC 数据读取\n","permalink":"https://0xpoe.dev/posts/2022-07-11-ticdc-%E6%9E%B6%E6%9E%84%E5%92%8C%E6%95%B0%E6%8D%AE%E5%90%8C%E6%AD%A5%E9%93%BE%E8%B7%AF%E8%A7%A3%E6%9E%90/","summary":"\u003cp\u003e\u003ca href=\"https://docs.pingcap.com/zh/tidb/dev/ticdc-overview\"\u003eTiCDC\u003c/a\u003e 是 \u003ca href=\"https://docs.pingcap.com/zh/tidb/dev/overview\"\u003eTiDB\u003c/a\u003e 生态中的一个数据同步工具，它通过拉取 \u003ca href=\"https://docs.pingcap.com/zh/tidb/dev/tikv-overview\"\u003eTiKV\u003c/a\u003e 的变更日志实现数据的增量同步。它除了可以将 TiDB 的数据同步至 \u003ca href=\"https://www.mysql.com/\"\u003eMySQL\u003c/a\u003e 兼容的数据库之外，还提供了同步至 \u003ca href=\"https://kafka.apache.org/\"\u003eKafka\u003c/a\u003e 的能力，支持 \u003ca href=\"https://github.com/alibaba/canal\"\u003ecanal\u003c/a\u003e 和 \u003ca href=\"https://avro.apache.org/\"\u003eavro\u003c/a\u003e 多种开放消息协议供其他系统订阅数据变更。\u003c/p\u003e\n\u003ch2 id=\"基本概念\"\u003e基本概念\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e以下这些都是理解 TiCDC 架构之前需要熟悉的基本概念，主要是对 TiDB 内部的一些组件和概念的解释。\u003c/p\u003e\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003eTiDB: 下文中提到的 TiDB 主要是指它作为是一个计算层的抽象提供执行 SQL 的能力，没有真实的存储数据。\u003c/li\u003e\n\u003cli\u003eTiKV: 一个支持 ACID 的键值数据库，它可以作为 TiDB 的存储层。\u003c/li\u003e\n\u003cli\u003eRegion: 它是 TiKV 数据移动的基本单位，可以将它认为是一组键值对集合。每个 Region 被复制到多个 TiKV 节点。\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://docs.pingcap.com/zh/tidb/dev/optimistic-transaction\"\u003e两阶段提交\u003c/a\u003e：一种分布式算法，它协调所有参与分布式原子事务的进程，决定是否提交或中止（回滚）该事务。\u003c/li\u003e\n\u003cli\u003eStartTs: TiDB 在开始两阶段提交时获取到的一个全局唯一递增的时间戳作为当前事务的唯一事务 ID，这里称为该事务的 start_ts。\u003c/li\u003e\n\u003cli\u003eCommitTs: TiDB 在提交两阶段提交事务时获取到另外一个全局唯一递增的时间戳作为该事务的 commit_ts。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"什么是-cdc\"\u003e什么是 CDC？\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://en.wikipedia.org/wiki/Change_data_capture\"\u003eCDC\u003c/a\u003e 的全称为 Change Data Capture，它是指从源数据库捕获数据并且将其同步到其他数据库或者应用程序的过程。它作为一种很常见的数据集成方式被大量的应用在\u003ca href=\"https://en.wikipedia.org/wiki/Data_warehouse\"\u003e数据仓库\u003c/a\u003e中。当然任何的数据库系统都可以构建自己的 CDC，比如 \u003ca href=\"https://docs.microsoft.com/en-us/sql/relational-databases/track-changes/about-change-data-capture-sql-server?view=sql-server-ver16\"\u003eSQL Server 的 CDC\u003c/a\u003e。TiCDC 就是专属于 TiDB 的 CDC，它的上游只能是 TiDB，但是它的下游可以是其他 MySQL 兼容的数据库系统，也可以是消息队列。\u003c/p\u003e\n\u003cp\u003e通过 TiCDC 我们可以实现 TiDB 集群之间的灾备和数据同步，也可以将 TiDB 的数据集成进其他数据处理系统。\u003c/p\u003e","title":"TiCDC 架构和数据同步链路解析"},{"content":"最近在改造 TiCDC 的 Sink 组件时需要为 MQ 类型的 Sink 接上一个 unbound 的 channel。 在搜索过程中发现了一个项目叫做 chann，它是一个接口统一并且支持范型的 channel。\n虽然这个库看上去实现很简单，但是我在实际使用过程中并不是很顺利。下面我就介绍一下我在使用该库时遇到的问题和进行的思考。\n此博客在 GitHub 上公开发布。 如果您有任何问题或疑问，请在此处打开一个 issue。\n简介 该库由 golang 社区非常活跃的 changkun 编写，托管在他组建的 golang-design 组织下。\n它提供了统一的接口来创建不同类型的 channel，并且支持范型：\nch := chann.New[int]() // 无界限, 无容量限制 ch := chann.New[func()](chann.Cap(0)) // 没有缓存, 容量为 0 ch := chann.New[string](chann.Cap(100)) // 有缓存，容量为 100 它的发送和接收操作和原生 channel 一致：\nch.In() \u0026lt;- 42 println(\u0026lt;-ch.Out()) // 42 它的关闭接口为：\nch.Close() 从接口来看，它几乎能无缝的接入到我当前的项目当中，这也是我选择它的原因。\n源码阅读 在开始分析我遇到的问题之前，我们需要先深入阅读一下源码。它的源码不是很长，并且我主要是用的是无界限的 chann，所以下面就着重看一下无界限的 chann 的源码。\ntype Chann[T any] struct { in, out chan T close chan struct{} cfg *config q []T } Chann 是一个范型结构体，它里面维护了 in 和 out channel，这就是我们能使用原生 channel 语法操作 chann 的入口和出口。\n另外一个比较关键的字段是 q，它将负责存储 chann 的缓存。\n它的构造方法：\ntype config struct { typ chanType len, cap int64 } type Opt func(*config) func New[T any](opts ...Opt) *Chann[T] { cfg := \u0026amp;config{ cap: -1, len: 0, typ: unbounded, } if len(opts) \u0026gt; 1 { panic(\u0026#34;chann: too many arguments\u0026#34;) } for _, o := range opts { o(cfg) } ch := \u0026amp;Chann[T]{cfg: cfg, close: make(chan struct{})} switch ch.cfg.typ { case unbuffered: ch.in = make(chan T) ch.out = ch.in case buffered: ch.in = make(chan T, ch.cfg.cap) ch.out = ch.in case unbounded: ch.in = make(chan T, 16) ch.out = make(chan T, 16) go ch.unboundedProcessing() } return ch 它的构造方法抽象出了一个 Opts 可选参数，根据它我们能够统一构造方法，我们不传递 Opts 参数使用默认 config 就表明创建了一个无界限的 channel。注意：如果我们创建的是无界限的 chann，那么它将启动一个 goroutine 来处理发送和接收数据。同时入口和出口 channel 的缓存长度为 16。\n接着是它的发送和接收接口：\nfunc (ch *Chann[T]) In() chan\u0026lt;- T { return ch.in } func (ch *Chann[T]) Out() \u0026lt;-chan T { return ch.out } 在这两个接口中我们就是直接返回了 in 和 out channel，这样我们就能直接使用 go 原生的 channel 语法来操作 chann。\n最后，我们来看一下它的核心逻辑，如何处理数据的发送和接收：\nfunc (ch *Chann[T]) unboundedProcessing() { var nilT T ch.q = make([]T, 0, 1\u0026lt;\u0026lt;10) for { select { case e, ok := \u0026lt;-ch.in: if !ok { panic(\u0026#34;chann: send-only channel ch.In() closed unexpectedly\u0026#34;) } atomic.AddInt64(\u0026amp;ch.cfg.len, 1) ch.q = append(ch.q, e) case \u0026lt;-ch.close: ch.unboundedTerminate() return } for len(ch.q) \u0026gt; 0 { select { case ch.out \u0026lt;- ch.q[0]: atomic.AddInt64(\u0026amp;ch.cfg.len, -1) ch.q[0] = nilT ch.q = ch.q[1:] case e, ok := \u0026lt;-ch.in: if !ok { panic(\u0026#34;chann: send-only channel ch.In() closed unexpectedly\u0026#34;) } atomic.AddInt64(\u0026amp;ch.cfg.len, 1) ch.q = append(ch.q, e) case \u0026lt;-ch.close: ch.unboundedTerminate() return } } if cap(ch.q) \u0026lt; 1\u0026lt;\u0026lt;5 { ch.q = make([]T, 0, 1\u0026lt;\u0026lt;10) } } } func (ch *Chann[T]) unboundedTerminate() { var nilT T close(ch.in) for e := range ch.in { ch.q = append(ch.q, e) } for len(ch.q) \u0026gt; 0 { select { case ch.out \u0026lt;- ch.q[0]: default: } ch.q[0] = nilT ch.q = ch.q[1:] } close(ch.out) close(ch.close) } 它的主逻辑就是一个 for 循环：\n初始化 q，初始容量为 1024 启动 for 循环， 进行 select 操作，如果有数据可以接收，则存入 q 缓存中，否则如果是 close 则进行关闭操作并退出 如果当前缓存中有数据，则启动内层循环 进行 select 操作，尝试发送缓存中的第一个元素，并且删除它。同时也会尝试接收新的数据，如果有新数据则存入 q 缓存中。另外，如果是 close 则进行关闭操作并退出 如果当前缓存中的数据已经全部发送完毕，则退出内层循环并且判断是否需要扩容，然后进入下一次主循环 它的关闭流程：\n关闭入口 channel，不再接收新的数据 将当前入口 channel 中的数据全部缓存起来等待处理，尝试优雅的停机 如果缓存中有数据的话，则启动 for 循环不断的将数据发送给出口 channel 最终所有数据都处理完毕，则关闭剩余 channel 以上就是它的核心实现，总的来说还是非常清晰和简单的。下面我们就来看看我在使用过程中遇到的问题！\n一个 🐛 首先是我在使用过程中遇到的一个 bug，issue 的链接：https://github.com/golang-design/chann/issues/3\n我和同事在使用 chann 过程中发现因为关闭操作不当会导致 cpu spin。我们在上面看的代码是目前最新的代码，但是在我报告这个 issue 之前它的 close 操作的核心逻辑是这样的：\nfor len(q) \u0026gt; 0 { select { case ch.out \u0026lt;- q[0]: q[0] = nilT // de-reference earlier to help GC q = q[1:] default: } } 请注意它和上面的操作完全是两个语义：\nfor len(q) \u0026gt; 0 { select { case ch.out \u0026lt;- q[0]: default: } q[0] = nilT // de-reference earlier to help GC q = q[1:] } 原来的写法，在 close 操作之后如果没有 receiver 继续接收数据，则 for 循环会一直进入 default 分支进行死循环，导致出现 cpu spin。\n而现在的做法是，当没有 receiver 时会进入 default 分支并且直接删除该数据，这样就能防止出现死循环导致 cpu spin。\n目前这样的做法也导致了语义的变化，我们现在实际上已经做不到优雅的停机了，因为很有可能在 default 分支被执行之后消费者丢失部分数据。具体的例子可以查看 changkun 的回复。\n目前这个问题还是没有被完全解决，在我个人来看，我更愿意保持原来的做法让用户来处置剩下的数据，这样语义更加明确。但是我们还是需要删除原来做法中的 default 分支，防止出现死循环。让用户来保证消费完所有剩余数据防止出现 goroutine 泄漏。\n但这只是我个人的想法，changkun 实际上不太认可这个做法，因为还是存在 goroutine 泄漏的风险。期待我们有更好的方案能彻底解决这个问题。\n因为该问题没有被解决，所以我决定将代码 fork 到 ticdc 仓库中，在这个过程中我又发现了一个不稳定的测试。\n一个不稳定测试 在 fork 代码的过程中，我发现 TestNonblockSelectRace 非常不稳定，在我们的 CI 系统中几乎稳定失败：\nfunc TestNonblockSelectRace(t *testing.T) { n := 1000 if testing.Short() { n = 1000 } done := chann.New[bool](chann.Cap(1)) for i := 0; i \u0026lt; n; i++ { c1 := chann.New[int]() c2 := chann.New[int]() c1.In() \u0026lt;- 1 go func() { runtime.Gosched() select { case \u0026lt;-c1.Out(): case \u0026lt;-c2.Out(): default: done.In() \u0026lt;- false return } done.In() \u0026lt;- true }() c2.In() \u0026lt;- 1 select { case \u0026lt;-c1.Out(): default: } if !\u0026lt;-done.Out() { t.Fatal(\u0026#34;no chan is ready\u0026#34;) } } } 这个测试的语义的保证是，当数据通过 In() 接口被写入之后，我们总是立马就能消费到该数据，但是实际上我们并不能提供这个语义保证，因为数据被写入之后还要等待被写入缓存中，才有可能被消费到。\n我做了一些修复降低了这个问题复现的频率，但是因为我们还是无法保证这个语义，所以最终 PR 还是没有被接受。\n最终 changkun 提交了代码明确了我们没有这个语义保证并且修复了这个不稳定测试。 新的语义如下：\n// An unbounded channel is not a buffered channel with infinite capacity, and they have different memory model semantics in terms of receiving a value: The recipient of a buffered channel is immediately available after a send is complete. However, the recipient of an unbounded channel may be available within a bounded time frame after a send is complete.\n他测试修改的也很巧妙：\nfunc TestNonblockSelectRace(t *testing.T) { n := 1000 done := chann.New[bool](chann.Cap(1)) for i := 0; i \u0026lt; n; i++ { c1 := chann.New[int]() c2 := chann.New[int]() // The input channel of an unbounded buffer have an internal // cache queue. When the input channel and the internal cache // queue both gets full, we are certain that once the next send // is complete, the out will be available for sure hence the // waiting time of a receive is bounded. for i := 0; i \u0026lt; internalCacheSize; i++ { c1.In() \u0026lt;- 1 } c1.In() \u0026lt;- 1 go func() { select { case \u0026lt;-c1.Out(): case \u0026lt;-c2.Out(): default: done.In() \u0026lt;- false return } done.In() \u0026lt;- true }() // Same for c2 for i := 0; i \u0026lt; internalCacheSize; i++ { c2.In() \u0026lt;- 1 } c2.In() \u0026lt;- 1 select { case \u0026lt;-c1.Out(): default: } if !\u0026lt;-done.Out() { t.Fatal(\u0026#34;no chan is ready\u0026#34;) } } } 因为我们的缓存队列大小是固定的，所以当我们确保缓存已满之后再写入时，我们总会有足够的时间收到写入的数据。\n学到的小技巧 在 fork 代码的过程中发现了几个 changkun 的代码中很有用的技巧。\n在切片中的数据被消费使用之后，将其设置为 nil，并且在容量不足时总是 make 新的切片，这样会帮助 GC 更快回收内存。 创建 export_test.go 文件导出测试专用函数，既能访问到内部状态，又不用将单元测试也放在同一个包下导致测试到处都依赖内部状态。 利用 runtime.GOMAXPROCS 和 runtime.NumGoroutine 来构造特定 goroutine 测试场景和检测 goroutine 泄漏。 参考链接 chann\nultimate-channel\nWill gc collect the objects while the array set to nil in Golang?\n","permalink":"https://0xpoe.dev/posts/2022-07-10-%E7%BB%99-ticdc-%E6%8E%A5%E4%B8%8A%E4%B8%80%E4%B8%AA-unbound-%E7%9A%84-channel/","summary":"\u003cp\u003e最近在改造 \u003ca href=\"https://github.com/pingcap/tiflow\"\u003eTiCDC\u003c/a\u003e 的 \u003ca href=\"https://ticdc-sink.slides.0xpoe.dev/\"\u003eSink\u003c/a\u003e 组件时需要为 \u003ca href=\"https://en.wikipedia.org/wiki/Message_queue\"\u003eMQ\u003c/a\u003e 类型的 Sink 接上一个 unbound 的 \u003ca href=\"https://go.dev/tour/concurrency/2\"\u003echannel\u003c/a\u003e。\n在搜索过程中发现了一个项目叫做 \u003ca href=\"https://github.com/golang-design/chann\"\u003echann\u003c/a\u003e，它是一个接口统一并且支持范型的 channel。\u003c/p\u003e\n\u003cp\u003e虽然这个库看上去实现很简单，但是我在实际使用过程中并不是很顺利。下面我就介绍一下我在使用该库时遇到的问题和进行的思考。\u003c/p\u003e\n\u003cp\u003e此博客在 \u003ca href=\"https://github.com/0xPoe/0xpoe.dev\"\u003eGitHub\u003c/a\u003e 上公开发布。 如果您有任何问题或疑问，请在此处打开一个 \u003ca href=\"https://github.com/0xPoe/0xpoe.dev/issues\"\u003eissue\u003c/a\u003e。\u003c/p\u003e\n\u003ch2 id=\"简介\"\u003e简介\u003c/h2\u003e\n\u003cp\u003e该库由 golang 社区非常活跃的 \u003ca href=\"https://github.com/changkun\"\u003echangkun\u003c/a\u003e 编写，托管在他组建的 \u003ca href=\"https://github.com/golang-design\"\u003egolang-design\u003c/a\u003e 组织下。\u003c/p\u003e\n\u003cp\u003e它提供了统一的接口来创建不同类型的 channel，并且支持范型：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nx\"\u003ech\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nx\"\u003echann\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eNew\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e]()\u003c/span\u003e                  \u003cspan class=\"c1\"\u003e// 无界限, 无容量限制\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nx\"\u003ech\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nx\"\u003echann\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eNew\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e\u003cspan class=\"p\"\u003e()](\u003c/span\u003e\u003cspan class=\"nx\"\u003echann\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eCap\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e   \u003cspan class=\"c1\"\u003e// 没有缓存, 容量为 0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nx\"\u003ech\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nx\"\u003echann\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eNew\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e](\u003c/span\u003e\u003cspan class=\"nx\"\u003echann\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eCap\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e100\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 有缓存，容量为 100\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e它的发送和接收操作和原生 channel 一致：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nx\"\u003ech\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eIn\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;-\u003c/span\u003e \u003cspan class=\"mi\"\u003e42\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;-\u003c/span\u003e\u003cspan class=\"nx\"\u003ech\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eOut\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 42\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e它的关闭接口为：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nx\"\u003ech\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eClose\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e从接口来看，它几乎能无缝的接入到我当前的项目当中，这也是我选择它的原因。\u003c/p\u003e\n\u003ch2 id=\"源码阅读\"\u003e源码阅读\u003c/h2\u003e\n\u003cp\u003e在开始分析我遇到的问题之前，我们需要先深入阅读一下源码。它的源码不是很长，并且我主要是用的是无界限的 chann，所以下面就着重看一下无界限的 chann 的源码。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"nx\"\u003eChann\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003eT\u003c/span\u003e \u003cspan class=\"kt\"\u003eany\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"kd\"\u003estruct\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ein\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eout\u003c/span\u003e \u003cspan class=\"kd\"\u003echan\u003c/span\u003e \u003cspan class=\"nx\"\u003eT\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eclose\u003c/span\u003e   \u003cspan class=\"kd\"\u003echan\u003c/span\u003e \u003cspan class=\"kd\"\u003estruct\u003c/span\u003e\u003cspan class=\"p\"\u003e{}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ecfg\u003c/span\u003e     \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003econfig\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003eq\u003c/span\u003e       \u003cspan class=\"p\"\u003e[]\u003c/span\u003e\u003cspan class=\"nx\"\u003eT\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eChann 是一个范型结构体，它里面维护了 \u003ccode\u003ein\u003c/code\u003e 和 \u003ccode\u003eout\u003c/code\u003e channel，这就是我们能使用原生 channel 语法操作 chann 的入口和出口。\u003c/p\u003e\n\u003cp\u003e另外一个比较关键的字段是 \u003ccode\u003eq\u003c/code\u003e，它将负责存储 chann 的缓存。\u003c/p\u003e\n\u003cp\u003e它的构造方法：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"nx\"\u003econfig\u003c/span\u003e \u003cspan class=\"kd\"\u003estruct\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003etyp\u003c/span\u003e      \u003cspan class=\"nx\"\u003echanType\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ecap\u003c/span\u003e \u003cspan class=\"kt\"\u003eint64\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003etype\u003c/span\u003e \u003cspan class=\"nx\"\u003eOpt\u003c/span\u003e \u003cspan class=\"kd\"\u003efunc\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e \u003cspan class=\"nx\"\u003eNew\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003eT\u003c/span\u003e \u003cspan class=\"kt\"\u003eany\u003c/span\u003e\u003cspan class=\"p\"\u003e](\u003c/span\u003e\u003cspan class=\"nx\"\u003eopts\u003c/span\u003e \u003cspan class=\"o\"\u003e...\u003c/span\u003e\u003cspan class=\"nx\"\u003eOpt\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nx\"\u003eChann\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ecfg\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003ecap\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003etyp\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eunbounded\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"nb\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eopts\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nb\"\u003epanic\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;chann: too many arguments\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"nx\"\u003e_\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eo\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"k\"\u003erange\u003c/span\u003e \u003cspan class=\"nx\"\u003eopts\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nf\"\u003eo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ecfg\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nx\"\u003ech\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"nx\"\u003eChann\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"nx\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e]{\u003c/span\u003e\u003cspan class=\"nx\"\u003ecfg\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003ecfg\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eclose\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003emake\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kd\"\u003echan\u003c/span\u003e \u003cspan class=\"kd\"\u003estruct\u003c/span\u003e\u003cspan class=\"p\"\u003e{})}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003eswitch\u003c/span\u003e \u003cspan class=\"nx\"\u003ech\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ecfg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003etyp\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ecase\u003c/span\u003e \u003cspan class=\"nx\"\u003eunbuffered\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003ech\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ein\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003emake\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kd\"\u003echan\u003c/span\u003e \u003cspan class=\"nx\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003ech\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eout\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003ech\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ein\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ecase\u003c/span\u003e \u003cspan class=\"nx\"\u003ebuffered\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003ech\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ein\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003emake\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kd\"\u003echan\u003c/span\u003e \u003cspan class=\"nx\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003ech\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ecfg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ecap\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003ech\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eout\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003ech\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ein\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ecase\u003c/span\u003e \u003cspan class=\"nx\"\u003eunbounded\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003ech\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ein\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003emake\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kd\"\u003echan\u003c/span\u003e \u003cspan class=\"nx\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e16\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"nx\"\u003ech\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eout\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003emake\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kd\"\u003echan\u003c/span\u003e \u003cspan class=\"nx\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e16\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003ego\u003c/span\u003e \u003cspan class=\"nx\"\u003ech\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eunboundedProcessing\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003ech\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e它的构造方法抽象出了一个 Opts 可选参数，根据它我们能够统一构造方法，我们不传递 Opts 参数使用默认 config 就表明创建了一个无界限的 channel。\u003cstrong\u003e注意：如果我们创建的是无界限的 chann，那么它将启动一个 goroutine 来处理发送和接收数据。同时入口和出口 channel 的缓存长度为 16。\u003c/strong\u003e\u003c/p\u003e","title":"给 TiCDC 接上一个 unbound 的 channel"},{"content":" ","permalink":"https://0xpoe.dev/presentations/2022-05-11-kafka/","summary":"\u003ciframe src=\"https://kafka.slides.0xpoe.dev/\" width=\"100%\" height=\"500px\" frameborder=\"0\" allowfullscreen\u003e\u003c/iframe\u003e","title":"Kafka: a Distributed Messaging System"},{"content":" ","permalink":"https://0xpoe.dev/presentations/2022-04-13-ticdc-sink/","summary":"\u003ciframe src=\"https://ticdc-sink.slides.0xpoe.dev/\" width=\"100%\" height=\"500px\" frameborder=\"0\" allowfullscreen\u003e\u003c/iframe\u003e","title":"TiCDC Sink Component"},{"content":" 免责声明：我既不懂 TiCDC，也不懂 Flink。所以下面的文章很有可能是一派胡言。\n最近我在帮 TiCDC 的 Kafka Sink 做多 Topic 支持，目标是让 TiCDC 能够把 TiDB 的数据接入到 Flink。其实现在的 TiCDC 就具备接入 Flink 的能力，只是受限于目前的 Kafka Sink 只能支持单个 Topic，接入和维护非常麻烦。\n我来介绍一下目前 TiCDC 接入 Flink 的现状和方法。\n此博客在 GitHub 上公开发布. 如果您有任何问题或疑问，请在此处打开一个 issue.\n简介 TiCDC Kafka Sink 支持多种协议格式，其中活跃维护的是 TiCDC 自己实现的 open-protocol 和阿里巴巴的 canal-json 协议。这次我们需要使用 canal-json 协议并通过 Flink 的 Kafka connector 接入 Flink 系统。\n启动 TiDB 和 TiCDC 集群 推荐使用 TiUP 启动 TiDB 和 TiCDC 集群，因为今天只是简单演示所以我就用 TiUP 的 Playground 启动测试集群。\n使用以下命令启动集群：\ntiup playground nightly --ticdc 1 这个命令首先会启动一个 TiDB playground 集群，其次因为我们指定了 --ticdc 1 所以它也会在集群中启动一个 TiCDC Server。\n如果你在使用这个命令的过程中遇到了类似如下的错误：\nError: Playground bootstrapping failed: version v5.4.0-nightly-20211225 on darwin/amd64 for component prometheus not found: unknown version 你可以尝试使用 tiup update playground 命令更新你的 Playground 组件之后再启动。启动完成之后控制台会输出：\nCLUSTER START SUCCESSFULLY, Enjoy it ^-^ To connect TiDB: mysql --comments --host 127.0.0.1 --port 62505 -u root -p (no password) To view the dashboard: http://127.0.0.1:2379/dashboard PD client endpoints: [127.0.0.1:2379] To view the Prometheus: http://127.0.0.1:9090 To view the Grafana: http://127.0.0.1:3000 现在我们就可以用 MySQL 客户端连接 TiDB 集群并查看版本：\nmysql --comments --host 127.0.0.1 --port 62505 -u root -p mysql\u0026gt; SELECT tidb_version(); +-----------------------------------------------------------------------------------------------------------------------+ | tidb_version() | +-----------------------------------------------------------------------------------------------------------------------+ | Release Version: v5.5.0-alpha Edition: Community Git Commit Hash: 23f7e51ae01287fa7f811e1462d8987e4d7727a4 Git Branch: heads/refs/tags/v5.5.0-alpha UTC Build Time: 2022-01-27 14:58:42 GoVersion: go1.16.4 Race Enabled: false TiKV Min Version: v3.0.0-60965b006877ca7234adaced7890d7b029ed1306 Check Table Before Drop: false | +-----------------------------------------------------------------------------------------------------------------------+ 1 row in set (0.00 sec) 可以看到我们启动了一个 nightly 版本的 TiDB 集群，我们使用 nightly 是因为过去几个月 TiCDC 团队对 canal-json 协议进行了大量的测试和问题修复，所以更推荐使用最新版本 TiCDC 的 canal-json 实现。 这些修复也将在 v5.4.0 发布。\n说到 v5.4.0 ，可能大家可以看到一个奇怪的现象，上面的版本都到 v5.5.0 了，但是 v5.4.0 还没发布。这跟 TiDB 社区目前的发布和分支维护模型有关。因为我们现在已经切出了 v5.4 分支开始了发布前的测试，所以现在 master 上的 nightly 就只能用 v5.5.0-alpha 的版本标签了。\n启动 Kafka 和 Flink 集群 在本地直接启动 Kafka 和 Flink 集群比较繁琐且坑比较多，所以我们可以使用 docker 和 docker-compose 快速的启动 Kafka 和 Flink 集群。\n我整理出了我在测试 Flink 时用到的 docker-compose，并将它分享到了 ticdc-test-compose。我们这次使用的是 Kafka connector 如果直接使用 Flink 的官方镜像，你在执行 SQL 的时候就会遇到 ClassNotFound 的 Java 异常，并且手动下载这些 Jar 包并重启 Flink 会比较麻烦，所以我自定义了一个 Dockerfile 来提前下载这些 Jar 包。\nFROM flink:scala_2.11-java11 RUN wget -P /opt/flink/lib/ https://repo1.maven.org/maven2/org/apache/kafka/kafka-clients/2.8.1/kafka-clients-2.8.1.jar RUN wget -P /opt/flink/lib/ https://repo1.maven.org/maven2/org/apache/flink/flink-connector-kafka_2.11/1.14.2/flink-connector-kafka_2.11-1.14.2.jar 我们在原镜像的基础上，下载了 Kafka 的客户端包和 Flink Kafka 连接器的包。docker-compose 文件就不展开讲了，都是一些最基本的配置。\n我们克隆该仓库之后在根目录通过一条命令就可以启动一个 Kafka 和 Flink 集群。\ngit clone https://github.com/0xPoe/ticdc-test-compose cd ticdc-test-compose docker-compose -f ./docker-compose-flink.yaml up -d 启动完成之后 docker ps:\n➜ ~ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 54ccfdccb655 ticdc-test-compose_taskmanager \u0026#34;/docker-entrypoint.…\u0026#34; 43 seconds ago Up 33 seconds 6123/tcp, 8081/tcp ticdc-test-compose-taskmanager-1 9fb8830ff91b ticdc-test-compose_jobmanager \u0026#34;/docker-entrypoint.…\u0026#34; 44 seconds ago Up 36 seconds 6123/tcp, 0.0.0.0:8081-\u0026gt;8081/tcp ticdc-test-compose-jobmanager-1 d89734157871 wurstmeister/kafka \u0026#34;kafka-console-consu…\u0026#34; 44 seconds ago Up 36 seconds ticdc-test-compose-kafka-consumer-1 10b933f61756 wurstmeister/kafka \u0026#34;start-kafka.sh\u0026#34; 45 seconds ago Up 38 seconds 0.0.0.0:9092-\u0026gt;9092/tcp ticdc-test-compose-kafka-1 839efd64fe11 wurstmeister/zookeeper \u0026#34;/bin/sh -c \u0026#39;/usr/sb…\u0026#34; 46 seconds ago Up 41 seconds 22/tcp, 2181/tcp, 2888/tcp, 3888/tcp ticdc-test-compose-zookeeper-1 可以看到我们启动了一个 Kafka 集群，其中包括一个 Server 和一个 Consumer，Server 会根据配置创建一个名为 ticdc-test 的 Topic，Consumer 也会在 console 中开始消费这个 Topic。 另外，我们也启动了 Flink 的 jobmanager 和 taskmanager。\n创建 Changefeed 开始同步数据到 Kafka 当 TiDB、Kafka 和 Flink 集群都启动之后，我们就可以使用 TiCDC 的 cli 工具创建 changefeed 进行数据同步。使用如下命令创建 changefeed：\n➜ ticdc-test-compose git:(main) tiup cdc:nightly cli changefeed create --sink-uri=\u0026#34;kafka://localhost:9092/ticdc-test?protocol=canal-json\u0026#34; The component `cdc` version v5.5.0-nightly-20220127 is not installed; downloading from repository. component cdc version v5.5.0-nightly-20220127 is already installed Starting component `cdc`: /Users/poe/.tiup/components/cdc/v5.5.0-nightly-20220127/cdc cli changefeed create --sink-uri=kafka://localhost:9092/ticdc-test?protocol=canal-json [2022/01/30 14:35:11.368 +08:00] [WARN] [kafka.go:383] [\u0026#34;topic\u0026#39;s `max.message.bytes` less than the user set `max-message-bytes`,use topic\u0026#39;s `max.message.bytes` to initialize the Kafka producer\u0026#34;] [max.message.bytes=3145728] [max-message-bytes=10485760] [2022/01/30 14:35:11.368 +08:00] [WARN] [kafka.go:393] [\u0026#34;topic already exist, TiCDC will not create the topic\u0026#34;] [topic=ticdc-test] [detail=\u0026#34;{\\\u0026#34;NumPartitions\\\u0026#34;:1,\\\u0026#34;ReplicationFactor\\\u0026#34;:1,\\\u0026#34;ReplicaAssignment\\\u0026#34;:{\\\u0026#34;0\\\u0026#34;:[1001]},\\\u0026#34;ConfigEntries\\\u0026#34;:{\\\u0026#34;max.message.bytes\\\u0026#34;:\\\u0026#34;3145728\\\u0026#34;,\\\u0026#34;segment.bytes\\\u0026#34;:\\\u0026#34;1073741824\\\u0026#34;}}\u0026#34;] Create changefeed successfully! ID: 040a4c2c-a829-42f6-b913-af9bb43c6ef4 Info: {\u0026#34;sink-uri\u0026#34;:\u0026#34;kafka://localhost:9092/ticdc-test?protocol=canal-json\u0026#34;,\u0026#34;opts\u0026#34;:{\u0026#34;max-message-bytes\u0026#34;:\u0026#34;3145728\u0026#34;},\u0026#34;create-time\u0026#34;:\u0026#34;2022-01-30T14:35:11.176117+08:00\u0026#34;,\u0026#34;start-ts\u0026#34;:430840089420759041,\u0026#34;target-ts\u0026#34;:0,\u0026#34;admin-job-type\u0026#34;:0,\u0026#34;sort-engine\u0026#34;:\u0026#34;unified\u0026#34;,\u0026#34;sort-dir\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;config\u0026#34;:{\u0026#34;case-sensitive\u0026#34;:true,\u0026#34;enable-old-value\u0026#34;:true,\u0026#34;force-replicate\u0026#34;:false,\u0026#34;check-gc-safe-point\u0026#34;:true,\u0026#34;filter\u0026#34;:{\u0026#34;rules\u0026#34;:[\u0026#34;*.*\u0026#34;],\u0026#34;ignore-txn-start-ts\u0026#34;:null},\u0026#34;mounter\u0026#34;:{\u0026#34;worker-num\u0026#34;:16},\u0026#34;sink\u0026#34;:{\u0026#34;dispatchers\u0026#34;:null,\u0026#34;protocol\u0026#34;:\u0026#34;canal-json\u0026#34;,\u0026#34;column-selectors\u0026#34;:null},\u0026#34;cyclic-replication\u0026#34;:{\u0026#34;enable\u0026#34;:false,\u0026#34;replica-id\u0026#34;:0,\u0026#34;filter-replica-ids\u0026#34;:null,\u0026#34;id-buckets\u0026#34;:0,\u0026#34;sync-ddl\u0026#34;:false},\u0026#34;scheduler\u0026#34;:{\u0026#34;type\u0026#34;:\u0026#34;table-number\u0026#34;,\u0026#34;polling-time\u0026#34;:-1},\u0026#34;consistent\u0026#34;:{\u0026#34;level\u0026#34;:\u0026#34;none\u0026#34;,\u0026#34;max-log-size\u0026#34;:64,\u0026#34;flush-interval\u0026#34;:1000,\u0026#34;storage\u0026#34;:\u0026#34;\u0026#34;}},\u0026#34;state\u0026#34;:\u0026#34;normal\u0026#34;,\u0026#34;error\u0026#34;:null,\u0026#34;sync-point-enabled\u0026#34;:false,\u0026#34;sync-point-interval\u0026#34;:600000000000,\u0026#34;creator-version\u0026#34;:\u0026#34;v5.5.0-alpha\u0026#34;} 我们成功的创建了 changefeed，但是出现了两条警告，第一条警告看起来比较诡异，因为我们并没有传递这个参数，我完了去报个 bug 修一修。第二条警告是说我们的 Topic 已经存在了，所以不会再自动创建该 Topic。\n在 changefeed 创建成功之后，我们就可以开始同步数据了。我们首先在 TiDB test 数据库中创建一个 simple1 表并插入数据：\nuse test; CREATE table test.simple1 ( id int primary key, val int ); INSERT INTO test.simple1(id, val) VALUES (1, 1); 这个时候数据已经写到 Kafka 了，我们使用 docker logs ticdc-test-compose-kafka-consumer-1 -f 就可以看到 Kafka console Consumer 已经输出了这两条变更:\nPartition:0\t{\u0026#34;id\u0026#34;:0,\u0026#34;database\u0026#34;:\u0026#34;test\u0026#34;,\u0026#34;table\u0026#34;:\u0026#34;simple1\u0026#34;,\u0026#34;pkNames\u0026#34;:null,\u0026#34;isDdl\u0026#34;:true,\u0026#34;type\u0026#34;:\u0026#34;CREATE\u0026#34;,\u0026#34;es\u0026#34;:1643525212536,\u0026#34;ts\u0026#34;:1643525215268,\u0026#34;sql\u0026#34;:\u0026#34;CREATE TABLE `test`.`simple1` (`id` INT PRIMARY KEY,`val` INT)\u0026#34;,\u0026#34;sqlType\u0026#34;:null,\u0026#34;mysqlType\u0026#34;:null,\u0026#34;data\u0026#34;:null,\u0026#34;old\u0026#34;:null} Partition:0\t{\u0026#34;id\u0026#34;:0,\u0026#34;database\u0026#34;:\u0026#34;test\u0026#34;,\u0026#34;table\u0026#34;:\u0026#34;simple1\u0026#34;,\u0026#34;pkNames\u0026#34;:[\u0026#34;id\u0026#34;],\u0026#34;isDdl\u0026#34;:false,\u0026#34;type\u0026#34;:\u0026#34;INSERT\u0026#34;,\u0026#34;es\u0026#34;:1643525217386,\u0026#34;ts\u0026#34;:1643525218526,\u0026#34;sql\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;sqlType\u0026#34;:{\u0026#34;id\u0026#34;:4,\u0026#34;val\u0026#34;:4},\u0026#34;mysqlType\u0026#34;:{\u0026#34;id\u0026#34;:\u0026#34;int\u0026#34;,\u0026#34;val\u0026#34;:\u0026#34;int\u0026#34;},\u0026#34;data\u0026#34;:[{\u0026#34;id\u0026#34;:\u0026#34;1\u0026#34;,\u0026#34;val\u0026#34;:\u0026#34;1\u0026#34;}],\u0026#34;old\u0026#34;:null} 使用 Flink SQL 建表查询 当数据写到 Kafka 之后，我们就可以启动 Flink SQL Client 进行建表和查询了。 首先我们需要进入 jobmanager 的容器内部：\ndocker exec -it ticdc-test-compose-jobmanager-1 bash 进入容器之后我们会在 flink 目录下，我们需要进入 bin 目录执行启动命令：\ncd bin ./sql-client.sh embedded 如果你的环境设置正确，我们现在应该就进入了 Flink SQL Client 的环境看到了小松鼠。\n接下来执行如下的建表 SQL:\nCREATE TABLE topic_test ( id int primary key, val int ) WITH ( \u0026#39;connector\u0026#39; = \u0026#39;kafka\u0026#39;, \u0026#39;topic\u0026#39; = \u0026#39;ticdc-test\u0026#39;, \u0026#39;properties.bootstrap.servers\u0026#39; = \u0026#39;kafka:9092\u0026#39;, \u0026#39;properties.group.id\u0026#39; = \u0026#39;testGroup\u0026#39;, \u0026#39;scan.startup.mode\u0026#39; = \u0026#39;earliest-offset\u0026#39;, \u0026#39;format\u0026#39; = \u0026#39;canal-json\u0026#39;, \u0026#39;canal-json.ignore-parse-errors\u0026#39; = \u0026#39;true\u0026#39; ); 对于这条命令，我们需要关注 WITH 中的这些参数：\n参数名 说明 connector 指定我们使用 Kafka connector topic 新建的这张表数据从 Kafka 的哪个 Topic 获取 properties.bootstrap.servers Kafka Broker 的连接地址，因为我们在 docker-compose 用了 bridge 的网络，所以使用服务名加端口即可 properties.group.id Kafka 消费组 ID scan.startup.mode Kafka consumer 的启动模式，从可能的最早偏移量开始 format 使用 canal-json 协议解析消息 canal-json.ignore-parse-errors 忽略掉解析错误，这在跳过和忽略一些不支持的 DDL 和 DML 时很有用 该命令执行成功之后我们就可以查询数据了：\nselect * from topic_test; {:height=\u0026ldquo;100px\u0026rdquo; width=\u0026ldquo;750px\u0026rdquo;}\n可以看到我们前面插入的数据已经正确的被解析和查询到了。现在当你在上游插入或者删除数据时，它也会实时更新数据。\n以上就是 TiCDC 将 TiDB 数据接入 Flink 的简单演示，但是目前由于 TiCDC 单个 changefeed 只支持同步到一个 Topic，所以在真实业务场景中可能需要创建大量的 changefeed 把每张表单独进行同步，然后在 Flink 再聚合使用。 目前 TiCDC 团队也正在实现单个 changefeed 的多 Topic 支持，希望我们能尽快的把它做完让用户更加方便的将数据接入 Flink。\n参考链接 Flink Docker Setup\nFlink Canal\n","permalink":"https://0xpoe.dev/posts/2022-01-30-%E4%BD%BF%E7%94%A8-ticdc-%E5%B0%86-tidb-%E6%8E%A5%E5%85%A5-flink/","summary":"\u003cblockquote\u003e\n\u003cp\u003e免责声明：我既不懂 TiCDC，也不懂 Flink。所以下面的文章很有可能是一派胡言。\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e最近我在帮 \u003ca href=\"https://docs.pingcap.com/tidb/stable/ticdc-overview/\"\u003eTiCDC\u003c/a\u003e 的 \u003ca href=\"https://docs.pingcap.com/tidb/stable/manage-ticdc#configure-sink-uri-with-kafka\"\u003eKafka Sink\u003c/a\u003e 做多 Topic 支持，目标是让 TiCDC 能够把 \u003ca href=\"https://docs.pingcap.com/tidb/stable/\"\u003eTiDB\u003c/a\u003e 的数据接入到 \u003ca href=\"https://flink.apache.org/\"\u003eFlink\u003c/a\u003e。其实现在的 TiCDC 就具备接入 Flink 的能力，只是受限于目前的 Kafka Sink\n只能支持单个 Topic，接入和维护非常麻烦。\u003c/p\u003e\n\u003cp\u003e我来介绍一下目前 TiCDC 接入 Flink 的现状和方法。\u003c/p\u003e\n\u003cp\u003e此博客在 \u003ca href=\"https://github.com/0xPoe/blog\"\u003eGitHub\u003c/a\u003e 上公开发布.\n如果您有任何问题或疑问，请在此处打开一个 \u003ca href=\"https://github.com/0xPoe/blog/issues\"\u003eissue\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"简介\"\u003e简介\u003c/h2\u003e\n\u003cp\u003eTiCDC Kafka Sink 支持多种协议格式，其中活跃维护的是 TiCDC 自己实现的 \u003ccode\u003eopen-protocol\u003c/code\u003e 和阿里巴巴的 \u003ccode\u003ecanal-json\u003c/code\u003e 协议。这次我们需要使用 \u003ccode\u003ecanal-json\u003c/code\u003e 协议并通过 Flink\n的 \u003ca href=\"https://nightlies.apache.org/flink/flink-docs-release-1.14/docs/connectors/table/kafka/\"\u003eKafka connector\u003c/a\u003e 接入 Flink 系统。\u003c/p\u003e\n\u003ch2 id=\"启动-tidb-和-ticdc-集群\"\u003e启动 TiDB 和 TiCDC 集群\u003c/h2\u003e\n\u003cp\u003e推荐使用 \u003ca href=\"https://tiup.io/\"\u003eTiUP\u003c/a\u003e 启动 TiDB 和 TiCDC 集群，因为今天只是简单演示所以我就用 TiUP 的 Playground 启动测试集群。\u003c/p\u003e\n\u003cp\u003e使用以下命令启动集群：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003etiup playground nightly --ticdc \u003cspan class=\"m\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e这个命令首先会启动一个 TiDB playground 集群，其次因为我们指定了 \u003ccode\u003e--ticdc 1\u003c/code\u003e 所以它也会在集群中启动一个 TiCDC Server。\u003c/p\u003e","title":"使用 TiCDC 将 TiDB 数据接入 Flink"},{"content":"最近应老板要求看了一个 X-lab 针对 GitHub 开源项目的分析报告，发现了一个背后非常好用并且强大的数据集。所以简单介绍一下这个数据集的收集方式和使用方法。\n此博客在 GitHub 上公开发布. 如果您有任何问题或疑问，请在此处打开一个 issue.\n简介 前段时间过年的时候老板给我发来了一个 X-lab 实验室做的 GitHub 开源项目的分析报告想让我看看我们是否可以学习和利用其中的分析方法和数据集。我就看了看这个报告，发现它其实背后是使用了另外一个开源项目 GH Archive 归档的数据集并配合 ClickHouse 生成了分析报告。所以我就简单的看了看这个项目，下面是这个 GitHub 归档项目的实现思路和使用方法。\nGH Archive 设计和实现思路 GH Archive 采取了一个非常暴力但是非常有效的归档方式，他们将 GitHub 所有的事件的都利用 GitHub 提供的 Event API 爬取了下来，然后按照 JSON 的格式存了下来。\n他们使用 ruby 来调用 GitHub API 进行爬取，具体的代码在 crawler.rb。有效的代码不超过一百行，所以就不具体展开解读了。主要就是调用 API 并且将数据按照日期整理为 JSON 格式的文件。\n同步至 BigQuery 数据集 GH Archive 通过爬取的方式整理好数据之后提供了 JSON 数据集的下载服务，我们可以通过 HTTP 客户端下载这些数据集，例如使用 wget:\nwget https://data.gharchive.org/2015-01-01-15.json.gz 这样我们就可以下载 2015 年 1 月 1 日 GitHub 所有公开仓库的所有事件。\n但是这样的 JSON 数据集分析起来不是很方便，还要配合其他的工具类进行分析。所以 GH Archive 就官方提供了一个免费公开的 Google BigQuery 数据集：githubarchive。\n数据集的结构主要还是按照时间范围来划分：\nday（e.g. 20150101） month（e.g. 201501） year（e.g. 2015） GH Archive 利用定时任务每一个小时同步一次数据到 BigQuery 数据集，他们将所有的 event 都构造成了同一个结构。\n这样就可以通过 BigQuery 来查询数据了，因为所有的事件的主要信息都存放在 payload 里面，我们可以利用 BigQuery 提供的 JSON 相关函数来处理和提取这些字段和内容。\n另外考虑到 GitHub 在不断的迭代，可能会对某些事件新增一些字段，所以 GH Archive 提供了一个 other 字段来存储这些新增的字段。\nBigQuery 数据集使用方法 通过以下几个步骤使用这个开放的数据集：\n登陆 Google 开发者控制台 如果没有项目的话创建一个新的项目并且激活使用 BigQuery API 打开数据集或者直接在项目中新建查询 执行以下查询： /* count of issues opened, closed, and reopened on 2019/01/01 */ SELECT event as issue_status, COUNT(*) as cnt FROM ( SELECT type, repo.name, actor.login,JSON_EXTRACT(payload, \u0026#39;$.action\u0026#39;) as event, FROM `githubarchive.day.20190101` WHERE type = \u0026#39;IssuesEvent\u0026#39;) GROUP by issue_status; 这样我们就成功的用上了这个公开的 BigQuery 数据集，下面我写几个简单的例子演示一下更多的查询：\n查询 1: 如何证明 PingCAP 节假日没上班？ 取样调查😂，我们查询一下 2021 年 1 月 1 日 TiDB 仓库有没有提交代码即可：\n/* 2021 年 1 月 1 日 TiDB 仓库代码 Push 次数 */ SELECT COUNT(*) as cnt FROM `githubarchive.day.20210101` WHERE type = \u0026#39;PushEvent\u0026#39; and org.login = \u0026#39;pingcap\u0026#39; and repo.name = \u0026#39;pingcap/tidb\u0026#39;; 结果为：cnt: 0\n查询 2: 想要 TiDB 2020 年的贡献者名单？ 我们查询 2020 年给 TiDB 提过 PR 的人员名单：\n/* 2020 年给 TiDB 提过 PR 的人员名单（只要提交就算） */ SELECT DISTINCT actor.login as name, FROM `githubarchive.year.2020` WHERE type = \u0026#39;PullRequestEvent\u0026#39; and org.login = \u0026#39;pingcap\u0026#39; and repo.name = \u0026#39;pingcap/tidb\u0026#39; and JSON_EXTRACT(payload, \u0026#39;$.action\u0026#39;) = \u0026#39;\u0026#34;opened\u0026#34;\u0026#39;; 结果为： 247 个人\n查询 3：TiDB 社区新的协作机器人上线后，3 月份至今还有多少人在手动合并 TiDB 的 PR？ 我们查询 3 月份至今所有 Push 事件的 actor 即可：\n/* 2021 年 3 月还在手动合并 PR 的人员名单 */ SELECT DISTINCT actor.login FROM `githubarchive.month.202103` WHERE type = \u0026#39;PushEvent\u0026#39; and org.login = \u0026#39;pingcap\u0026#39; and repo.name = \u0026#39;pingcap/tidb\u0026#39;; 结果为：除了机器人外，还有 6 个人。\n[ { \u0026#34;login\u0026#34;: \u0026#34;AndreMouche\u0026#34; }, { \u0026#34;login\u0026#34;: \u0026#34;qw4990\u0026#34; }, { \u0026#34;login\u0026#34;: \u0026#34;hanfei1991\u0026#34; }, { \u0026#34;login\u0026#34;: \u0026#34;ti-chi-bot\u0026#34; }, { \u0026#34;login\u0026#34;: \u0026#34;AilinKid\u0026#34; }, { \u0026#34;login\u0026#34;: \u0026#34;tiancaiamao\u0026#34; }, { \u0026#34;login\u0026#34;: \u0026#34;eurekaka\u0026#34; } ] 有了这些所有事件的归档，我们可以从很多的角度去分析社区的运行状况。同时因为 payload 的信息非常全面，我们甚至能够通过项目的 ID 或者用户的 ID 去仔细查询分析所有的事件，不会有数据因为项目重命名或者迁移组织而无法查询。\n与其他工具结合 因为 GH Archive 存储的是 JSON 格式，所以我们完全可以将数据导入到其他工具中进行分析处理，比如 X-lab 就将数据导入了他们自己的 ClickHouse，并且按照他们的报告需求对数据进行了分析。另外也有开源组织创建了一个公开的 ClickHouse 数据集并且创建了大量的查询和分析样例。\n更多与其他工具结合的例子可以参考 GH Archive 官网中的资源。\n我会继续探索 GH Archive 在 TiDB 社区落地的方案，就目前来看完全可以满足我们对社区运营的数据支撑需求。后续我也会更新 GH Archive 在 TiDB 社区应用的场景和具体落地方案。\n参考链接 github-analysis-report\ngharchive.org\nEverything You Always Wanted To Know About GitHub\n","permalink":"https://0xpoe.dev/posts/2021-03-07-%E5%BC%80%E6%BA%90%E7%A4%BE%E5%8C%BA%E5%88%86%E6%9E%90%E6%8A%A5%E5%91%8A%E8%83%8C%E5%90%8E%E7%9A%84%E6%95%B0%E6%8D%AE%E6%94%AF%E6%92%91-gh-archive/","summary":"\u003cp\u003e最近应老板要求看了一个 X-lab 针对 GitHub 开源项目的\u003ca href=\"http://www.x-lab.info/github-analysis-report/#/report\"\u003e分析报告\u003c/a\u003e，发现了一个背后非常好用并且强大的\u003ca href=\"https://www.gharchive.org/\"\u003e数据集\u003c/a\u003e。所以简单介绍一下这个数据集的收集方式和使用方法。\u003c/p\u003e\n\u003cp\u003e此博客在 \u003ca href=\"https://github.com/0xPoe/blog\"\u003eGitHub\u003c/a\u003e 上公开发布. 如果您有任何问题或疑问，请在此处打开一个 \u003ca href=\"https://github.com/0xPoe/blog/issues\"\u003eissue\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"简介\"\u003e简介\u003c/h2\u003e\n\u003cp\u003e前段时间过年的时候老板给我发来了一个 \u003ca href=\"http://www.x-lab.info/\"\u003eX-lab\u003c/a\u003e 实验室做的 GitHub 开源项目的\u003ca href=\"http://www.x-lab.info/github-analysis-report/#/report\"\u003e分析报告\u003c/a\u003e想让我看看我们是否可以学习和利用其中的分析方法和数据集。我就看了看这个报告，发现它其实背后是使用了另外一个开源项目 \u003ca href=\"https://github.com/igrigorik/gharchive.org\"\u003eGH Archive\u003c/a\u003e 归档的数据集并配合 ClickHouse 生成了分析报告。所以我就简单的看了看这个项目，下面是这个 GitHub 归档项目的实现思路和使用方法。\u003c/p\u003e\n\u003ch2 id=\"gh-archive-设计和实现思路\"\u003eGH Archive 设计和实现思路\u003c/h2\u003e\n\u003cp\u003eGH Archive 采取了一个非常暴力但是非常有效的归档方式，他们将 GitHub 所有的事件的都利用 GitHub 提供的 \u003ca href=\"https://api.github.com/events\"\u003eEvent API\u003c/a\u003e 爬取了下来，然后按照 JSON 的格式存了下来。\u003c/p\u003e\n\u003cp\u003e他们使用 ruby 来调用 GitHub API 进行爬取，具体的代码在 \u003ca href=\"https://github.com/igrigorik/gharchive.org/blob/master/crawler/crawler.rb\"\u003ecrawler.rb\u003c/a\u003e。有效的代码不超过一百行，所以就不具体展开解读了。\u003cstrong\u003e主要就是调用 API 并且将数据按照日期整理为 JSON 格式的文件。\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"同步至-bigquery-数据集\"\u003e同步至 BigQuery 数据集\u003c/h2\u003e\n\u003cp\u003eGH Archive 通过爬取的方式整理好数据之后提供了 JSON 数据集的下载服务，我们可以通过 HTTP 客户端下载这些数据集，例如使用 \u003ccode\u003ewget\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ewget https://data.gharchive.org/2015-01-01-15.json.gz\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e这样我们就可以下载 2015 年 1 月 1 日 GitHub 所有公开仓库的所有事件。\u003c/p\u003e\n\u003cp\u003e但是这样的 JSON 数据集分析起来不是很方便，还要配合其他的工具类进行分析。所以 GH Archive 就官方提供了一个免费公开的 Google BigQuery 数据集：githubarchive。\u003c/p\u003e","title":"开源社区分析报告背后的数据支撑: GH Archive"},{"content":"大家好，我是 Dongpo. 最近开始做贵司推出的 TiDB High Performance 课程，所以开个课程实验记录的坑！\n此博客在 GitHub 上公开发布. 如果您有任何问题或疑问，请在此处打开一个 issue.\n简介 在高性能挑战赛的 文档 中找到第一节课的实验描述，实验需要分别下载和编译 TiDB, TiKV 和 PD， 并且需要修改 TiDB 源码让其在启动事务的时候，打印一句 hello transation 的日志。下面我就简单记录一下整个实验过程。\n克隆源码并编译 需要分别克隆和编译 TiDB, TiKV 和 PD. 这三个库分别对应了 TiDB 中的计算，存储和调度三个层面。具体内容可以参考课程文档中对应的三篇文章。\n编译 TiDB git clone https://github.com/0xPoe/tidb 在编译之前，需要我们安装 make 工具，因为三个项目的 build 都是用 makefile 来组织的。查看 makefile 可以看到 .PHONY 中有个 server 的伪目标。内容如下：\nserver: ifeq ($(TARGET), \u0026#34;\u0026#34;) CGO_ENABLED=1 $(GOBUILD) $(RACE_FLAG) -ldflags \u0026#39;$(LDFLAGS) $(CHECK_FLAG)\u0026#39; -o bin/tidb-server tidb-server/main.go else CGO_ENABLED=1 $(GOBUILD) $(RACE_FLAG) -ldflags \u0026#39;$(LDFLAGS) $(CHECK_FLAG)\u0026#39; -o \u0026#39;$(TARGET)\u0026#39; tidb-server/main.go endif 可以看到这就是编译 TiDB server 的命令，该命令会把 server 编译到 bin 目录下。但是实际上在 TiDB 的伪目标中原来还有个 build 的命令， 它是个无用的命令，没有地方用到了这个命令。所以我就提交了个 PR 把它删除了。运行使用 make server 即可。\nmake server 在编译完成后，bin 目录如下所示：\n➜ bin git:(master) ✗ tree . └── tidb-server 0 directories, 1 file 编译 TiKV git clone https://github.com/0xPoe/tikv 在编译之前我也是需要安装好 build TiKV 项目工具链，可以在 CONTRIBUTING 里面找到一些需要的依赖。 主要就是再装一个 cmake，安装完成之后我们就可以进行 TiKV 的编译了。查看 makefile 找到编译命令如下：\nbuild: cargo build --no-default-features --features \u0026#34;${ENABLE_FEATURES}\u0026#34; TiKV 这边的比较直观，就是叫 build。实际上就是运行 cargo 的 build 命令。运行 make build.\nmake build cargo 编译完的二进制文件在 target 中，目录如下所示：\n➜ debug git:(master) ls build incremental tikv-ctl tikv-server deps libcmd.d tikv-ctl.d tikv-server.d examples libcmd.rlib tikv-ctl.dSYM tikv-server.dSYM 可以找到名字为 tikv-server 的二进制文件。\n编译 PD git clone https://github.com/0xPoe/pd PD 中也没有特殊的编译工具链要求，直接使用查看 makefile 找到编译的命令：\nbuild: pd-server pd-ctl pd-recover pd-server: export GO111MODULE=on pd-server: ${PD_SERVER_DEP} CGO_ENABLED=$(BUILD_CGO_ENABLED) go build $(BUILD_FLAGS) -gcflags \u0026#39;$(GCFLAGS)\u0026#39; -ldflags \u0026#39;$(LDFLAGS)\u0026#39; -tags \u0026#34;$(BUILD_TAGS)\u0026#34; -o bin/pd-server cmd/pd-server/main.go 运行 make build 得到如下的目录：\n➜ bin git:(master) tree . ├── pd-ctl ├── pd-recover └── pd-server 使用 TiUP 部署本地集群 可以尝试使用 tiup 来部署本地集群测试。TiUP 是贵司在 TiDB 4.0 时研发的一个新的部署工具，据说是受到 rustup 的启发做的。 利用 TiUP 使用自己编译的二进制文件即可快速的部署一套本地的集群测试。但是似乎 TiUP 官网的文档链接主要是针服务器上的部署，但是可以在 TiDB in action 的 TiUP 部署本地测试环境章节中找到通过指定二进制文件路径来启动本地集群的参数。\n参数如下所示：\nUsage: tiup playground [version] [flags] Flags: --db.binpath string TiDB instance binary path --kv.binpath string TiKV instance binary path --pd.binpath string PD instance binary path 通过这三个参数，可以快速的利用 TiUP 使用自己编译的二进制文件启动本地集群。使用如下命令启动集群：\n➜ ~ tiup playground --db.binpath /Users/poe/GolandProjects/tidb/bin/tidb-server --kv.binpath /Users/poe/ClionProjects/tikv/target/debug/tikv-server --pd.binpath /Users/poe/GolandProjects/pd/bin/pd-server 输出以下内容之后启动成功：\nCLUSTER START SUCCESSFULLY, Enjoy it ^-^ To connect TiDB: mysql --host 127.0.0.1 --port 4000 -u root To view the dashboard: http://127.0.0.1:2379/dashboard To view the Prometheus: http://127.0.0.1:9090 To view the Grafana: http://127.0.0.1:3000 启动成功后使用 mysql 客户端连接成功：\n➜ ~ mysql --host 127.0.0.1 --port 4000 -u root Welcome to the MySQL monitor. Commands end with ; or \\g. Your MySQL connection id is 2 Server version: 5.7.25-TiDB-v4.0.0-beta.2-1101-gf82e5320a-dirty TiDB Server (Apache License 2.0) Community Edition, MySQL 5.7 compatible Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type \u0026#39;help;\u0026#39; or \u0026#39;\\h\u0026#39; for help. Type \u0026#39;\\c\u0026#39; to clear the current input statement. mysql\u0026gt; use test; Database changed 输出 hello transation 输出到 warnings 原来的题目描述中其实并没有要求输出到日志中，所以我想要不直接输出到 warnings 中。为了找到与事务开始的相关的代码，我先去 ast 包中搜索 begin 关键字找到了 begin 语句的定义。 然后通过 GoLand 搜索引用，找到了相关的引用：\n通过引用找到了执行 begin 语句的函数，它被定义在 simple.go 中。尝试直接添加输出到 warnings：\n// FILE: tidb/executor/simple.go func (e *SimpleExec) executeBegin(ctx context.Context, s *ast.BeginStmt) error { // ... e.ctx.GetSessionVars().StmtCtx.AppendNote(errors.New(\u0026#34;hello transaction\u0026#34;)) // ... } 从当前执行器中获取语句 Context 然后在其中追加一句 Note，这样就可以在 begin 语句执行之后，在 warnings 中找到这个 Note. 启动集群尝试执行一个事务：\n➜ ~ mysql --host 127.0.0.1 --port 4000 -u root mysql\u0026gt; use test; Database changed mysql\u0026gt; begin; Query OK, 0 rows affected, 1 warning (0.00 sec) mysql\u0026gt; show warnings; +-------+------+-------------------+ | Level | Code | Message | +-------+------+-------------------+ | Note | 1105 | hello transaction | +-------+------+-------------------+ 1 row in set (0.00 sec) 这样我们就实现了在事务 begin 语句执行后输出一句 hello transation.\n输出到日志 后来题目改了要求，需要将 hell transation 输出到日志当中。输出到日志当中也十分简单，可以同样在 simple.go 的 executeRollback 找到如何打印日志的例子：\n// FILE: tidb/executor/simple.go func (e *SimpleExec) executeRollback(s *ast.RollbackStmt) error { // ... logutil.BgLogger().Debug(\u0026#34;execute rollback statement\u0026#34;, zap.Uint64(\u0026#34;conn\u0026#34;, sessVars.ConnectionID)) // ... } 我们也可以在 executeBegin 中尝试添加日志：\n// FILE: tidb/executor/simple.go func (e *SimpleExec) executeBegin(ctx context.Context, s *ast.BeginStmt) error { // ... logutil.BgLogger().Info(\u0026#34;hello transaction\u0026#34;) // ... } 再次提交事务测试后，可以去 .tiup 目录中找到该日志：\n➜ tidb-0 pwd /Users/poe/.tiup/data/SA3l5r6/tidb-0 ➜ tidb-0 less tidb.log # 搜索 hello 找到日志 [2020/09/09 11:51:23.426 +08:00] [INFO] [simple.go:584] [\u0026#34;hello transaction\u0026#34;] 这样就输出了 hello transaction 到日志中。\n到这基本上就算是完成了实验，但是整个实验中忽略了自动提交的情况。目前最大的收获是学会了用 TiUP 快速的部署和搭建本地的测试环境。\n参考链接 三篇文章了解 TiDB 技术内幕 - 说存储\n三篇文章了解 TiDB 技术内幕 - 说计算\n三篇文章了解 TiDB 技术内幕 - 谈调度\nTiDB In Action: based on 4.0\n","permalink":"https://0xpoe.dev/posts/2020-09-03-tidb-high-performance-%E8%AF%BE%E7%A8%8B%E5%AE%9E%E9%AA%8C-1/","summary":"\u003cp\u003e大家好，我是 \u003ca href=\"https://github.com/0xPoe\"\u003eDongpo\u003c/a\u003e. 最近开始做贵司推出的 TiDB High Performance 课程，所以开个课程实验记录的坑！\u003c/p\u003e\n\u003cp\u003e此博客在 \u003ca href=\"https://github.com/0xPoe/blog\"\u003eGitHub\u003c/a\u003e 上公开发布. 如果您有任何问题或疑问，请在此处打开一个 \u003ca href=\"https://github.com/0xPoe/blog/issues\"\u003eissue\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"简介\"\u003e简介\u003c/h2\u003e\n\u003cp\u003e在高性能挑战赛的 \u003ca href=\"https://docs.qq.com/sheet/DSlBwS3VCb01kTnZw?tab=BB08J2\"\u003e文档\u003c/a\u003e 中找到第一节课的实验描述，实验需要分别下载和编译 TiDB, TiKV 和 PD，\n并且需要修改 TiDB 源码让其在启动事务的时候，打印一句 \u003ccode\u003ehello transation\u003c/code\u003e 的日志。下面我就简单记录一下整个实验过程。\u003c/p\u003e\n\u003ch2 id=\"克隆源码并编译\"\u003e克隆源码并编译\u003c/h2\u003e\n\u003cp\u003e需要分别克隆和编译 \u003ca href=\"https://github.com/pingcap/tidb\"\u003eTiDB\u003c/a\u003e, \u003ca href=\"https://github.com/tikv/tikv\"\u003eTiKV\u003c/a\u003e 和 \u003ca href=\"https://github.com/tikv/pd\"\u003ePD\u003c/a\u003e.\n这三个库分别对应了 TiDB 中的计算，存储和调度三个层面。具体内容可以参考课程文档中对应的三篇文章。\u003c/p\u003e\n\u003ch3 id=\"编译-tidb\"\u003e编译 TiDB\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003egit clone https://github.com/0xPoe/tidb\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e在编译之前，需要我们安装 \u003cstrong\u003emake\u003c/strong\u003e 工具，因为三个项目的 build 都是用 makefile 来组织的。查看 makefile 可以看到 \u003ccode\u003e.PHONY\u003c/code\u003e 中有个 server\n的伪目标。内容如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-makefile\" data-lang=\"makefile\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nf\"\u003eserver\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003eifeq\u003c/span\u003e \u003cspan class=\"err\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003e$(\u003c/span\u003e\u003cspan class=\"nv\"\u003eTARGET\u003c/span\u003e\u003cspan class=\"k\"\u003e)\u003c/span\u003e\u003cspan class=\"err\"\u003e,\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;\u0026#34;\u003c/span\u003e\u003cspan class=\"err\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nv\"\u003eCGO_ENABLED\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"m\"\u003e1\u003c/span\u003e \u003cspan class=\"k\"\u003e$(\u003c/span\u003eGOBUILD\u003cspan class=\"k\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003e$(\u003c/span\u003eRACE_FLAG\u003cspan class=\"k\"\u003e)\u003c/span\u003e -ldflags \u003cspan class=\"s1\"\u003e\u0026#39;$(LDFLAGS) $(CHECK_FLAG)\u0026#39;\u003c/span\u003e -o bin/tidb-server tidb-server/main.go\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003eelse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nv\"\u003eCGO_ENABLED\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"m\"\u003e1\u003c/span\u003e \u003cspan class=\"k\"\u003e$(\u003c/span\u003eGOBUILD\u003cspan class=\"k\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003e$(\u003c/span\u003eRACE_FLAG\u003cspan class=\"k\"\u003e)\u003c/span\u003e -ldflags \u003cspan class=\"s1\"\u003e\u0026#39;$(LDFLAGS) $(CHECK_FLAG)\u0026#39;\u003c/span\u003e -o \u003cspan class=\"s1\"\u003e\u0026#39;$(TARGET)\u0026#39;\u003c/span\u003e tidb-server/main.go\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003eendif\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e可以看到这就是编译 TiDB server 的命令，该命令会把 server 编译到 bin 目录下。但是实际上在 TiDB 的伪目标中原来还有个 build 的命令，\n它是个无用的命令，没有地方用到了这个命令。所以我就提交了个 \u003ca href=\"https://github.com/pingcap/tidb/pull/19221\"\u003ePR\u003c/a\u003e 把它删除了。运行使用 \u003ccode\u003emake server\u003c/code\u003e 即可。\u003c/p\u003e","title":"TiDB High Performance 课程实验 1"},{"content":"大家好，我是 Dongpo 。今天想跟大家简单介绍一下如何使用一些 golang 的静态代码检查工具来提升代码质量！\n此博客在 GitHub 上公开发布. 如果您有任何问题或疑问，请在此处打开一个 issue。\n简介 从去年接触到 TiDB 就开始尝试在社区帮忙修复一些简单的 Bug。最近，我在阅读代码的过程中发现 TiDB 的代码库中有大量的没有必要的类型转换，我就用 GoLand 分析检查出大部分的无效的类型转化， 然后提了一个 PR （CEO 半夜 review 代码，哈哈哈）修复。在这个 PR 中 zz-jason 大神 评论希望能够通过静态检查工具来检测无效的类型转换。\n我经过一些研究，决定使用 unconvert 来检测无效的类型转换，然后在这个 PR 解决了这个问题。 最近我终于有机会在公司写 Go了，所以我也想在公司的项目上配置和使用一些静态检查工具来提升代码质量。 在经过一下午的努力之后终于把 TiDB 的大部分检查工具移植到了公司项目上，并且在 github 上创建了一个模板项目 go-boilerplate 。下面我就简单介绍一下这个模板的构建过程和使用的方式。\ninit 项目，添加代码 我最近使用的 Go 版本 1.13.8，所以就使用 go mod 来初始化和管理项目。\ngo mod init github.com/0xPoe/go-boilerplate 然后我添加了 main 文件和一个用作例子的 foo 文件，目录结构如下所示：\n. ├── foo.go ├── foo_test.go ├── go.mod ├── go.sum ├── LICENSE ├── main.go ├── Makefile ├── README.md 我在 main 文件中只是简单的输出一句话：\n// main.go package main import ( \u0026#34;fmt\u0026#34; ) func main() { fmt.Println(\u0026#34;I love Rust!\u0026#34;) } 在 foo 函数我为了测试 go tidy 功能，专门引入了一个第三方的随机生成测试数据的库：go get github.com/Pallinder/go-randomdata。在代码中简单的生成一个随机数并测试：\n// foo.go package main import ( \u0026#34;github.com/Pallinder/go-randomdata\u0026#34; ) func foo() int { return randomdata.Number(20) } // foo_test.go package main import \u0026#34;testing\u0026#34; func TestFoo(t *testing.T) { testFoo := foo() if testFoo \u0026lt; 0 || testFoo \u0026gt; 20 { t.Error(\u0026#34;The value should more than 0 and less than 20!\u0026#34;) } } 添加 Makefile，开始构建 在初始化完项目之后，我们需要添加一个 Makefile 来构建项目，我接下来所有的检查工具都是通过 Makefile 来组织和构建的。在 Makefile 中我们要先定义一些基础的通用的变量：\n# 定义项目名称 PROJECT=go-boilerplate GOPATH ?= $(shell go env GOPATH) P=8 # 确保 GOPATH 已经设置好了 ifeq \u0026#34;$(GOPATH)\u0026#34; \u0026#34;\u0026#34; $(error Please set the environment variable GOPATH before running `make`) endif # 校验输出是否正常。 FAIL_ON_STDOUT := awk \u0026#39;{ print } END { if (NR \u0026gt; 0) { exit 1 } }\u0026#39; # 设置环境变量 path_to_add := $(addsuffix /bin,$(subst :,/bin:,$(GOPATH))):$(PWD)/tools/bin export PATH := $(path_to_add):$(PATH) # 一些 Go 相关的命令 GO := GO111MODULE=on go GOBUILD := $(GO) build $(BUILD_FLAG) -tags codes GOTEST := $(GO) test -p $(P) # 获取项目的包和包中的文件 PACKAGE_LIST := go list ./... PACKAGES := $$($(PACKAGE_LIST)) PACKAGE_DIRECTORIES := $(PACKAGE_LIST) | sed \u0026#39;s|github.com/0xPoe/$(PROJECT)||\u0026#39; FILES := $$(find $$($(PACKAGE_DIRECTORIES)) -name \u0026#34;*.go\u0026#34;) 这些通用的变量中有两个地方需要注意：\n我们定义了一个 FAIL_ON_STDOUT 的 awk 命令，该命令会检测是否有错误信息输出，我们在后面会多次使用到该命令。NR 是内置的变量表示 number of record。如果我们检测到其他输出信息就失败退出（输出信息也就是错误信息，我们会做输出重定向）。 我们匹配和查找出了包下的所有 go 文件，因为我这只是个简单的模板项目没有其他的 package，所以我在 sed 中只替换和匹配了第一层 package：sed 's|github.com/0xPoe/$(PROJECT)||'。如果你有很多子 package，就需要修改这个替换规则。 添加工具，创建命令 我们需要用 go get 获取静态检查工具，但是为了不污染我们项目的依赖（因为我们在代码中实际上并没有用到这些包），我们可以创建一个 tools/check 的目录并且在其中创建一个 mod 来管理这些静态检查工具。\nmkdir tools/check cd tools/check go mod init github.com/0xPoe/go-boilerplate/_tools 初始化 tools 模块之后目录结构如下：\n. ├── foo.go ├── foo_test.go ├── go.mod ├── go.sum ├── LICENSE ├── main.go ├── Makefile ├── README.md └── tools └── check └── go.mod 有了该模块，我们就可以将工具直接编译到其目录下来使用。下面我就开始介绍目前我的模板项目中用到的一些很有帮助的检查工具：\n1.gofmt\nfmt: @echo \u0026#34;gofmt (simplify)\u0026#34; @gofmt -s -l -w $(FILES) 2\u0026gt;\u0026amp;1 | $(FAIL_ON_STDOUT) 首先就是 Go 自带的 fmt 检查代码格式并输出错误，通过 2\u0026gt;\u0026amp;1 将标准错误输出重定向到标准输出，然后利用我们上面定义的 FAIL_ON_STDOUT 命令来检测结果。\n2.goword\n# 编译到 tool/bin 目录下以供使用 tools/bin/goword: tools/check/go.mod cd tools/check; \\ $(GO) build -o ../bin/goword github.com/chzchzchz/goword # 先调用上面的编译命令，再调用进行检测 goword:tools/bin/goword tools/bin/goword $(FILES) 2\u0026gt;\u0026amp;1 | $(FAIL_ON_STDOUT) 该工具主要是检测你代码中 godoc 函数注释的拼写错误，在这里我们检测了包下的所有文件。\n3.gosec\n# 编译到 tool/bin 目录下以供使用 tools/bin/gosec: tools/check/go.mod cd tools/check; \\ $(GO) build -o ../bin/gosec github.com/securego/gosec/cmd/gosec # 先调用上面的编译命令，再调用进行检测 gosec:tools/bin/gosec tools/bin/gosec ./... 该工具主要是检测你代码中可能的安全问题，在这里我们使用了 ./.. 匹配所有文件。\n4.golangci-lint\n# 使用官方提供的安装脚本安装 tools/bin/golangci-lint: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b ./tools/bin v1.21.0 # 先调用上面的安装命令，再调用进行检测 check-static: tools/bin/golangci-lint tools/bin/golangci-lint run -v --disable-all --deadline=3m \\ --enable=misspell \\ --enable=ineffassign \\ $$($(PACKAGE_DIRECTORIES)) 该工具非常强大并且它是插件化的，可以集成使用很多其他的检查工具，在这里我们用 --disable-all 先关掉了所有的插件，然后用 --enable=misspell --enable=ineffassign 只打开这两个检测工具去检测拼写错误和无效的变量分配和赋值。\n这个工具实际上能集成几乎所有的静态检查工具，而且还可以做 CI 支持。如果你的项目足够复杂建议直接启用所有插件，使用这一个工具就能搞定大部分问题。\n5.errcheck\n# 编译到 tool/bin 目录下以供使用 tools/bin/errcheck: tools/check/go.mod cd tools/check; \\ $(GO) build -o ../bin/errcheck github.com/kisielk/errcheck # 先调用上面的编译命令，再调用进行检测 errcheck:tools/bin/errcheck @echo \u0026#34;errcheck\u0026#34; @GO111MODULE=on tools/bin/errcheck -exclude ./tools/check/errcheck_excludes.txt -ignoretests -blank $(PACKAGES) 该工具主要是做错误处理的检测，强制要求你处理错误。可以看到我们在命令参数中还排除了一些错误处理的检测。如果有些错误你不想处理即可将其放入其中。注意我们在这里检测的是 PACKAGES。\n# errcheck_excludes.txt fmt.Fprintf fmt.Fprint fmt.Sscanf 6.unconvert\n# 编译到 tool/bin 目录下以供使用 tools/bin/unconvert: tools/check/go.mod cd tools/check; \\ $(GO) build -o ../bin/unconvert github.com/mdempsky/unconvert # 先调用上面的编译命令，再调用进行检测 unconvert:tools/bin/unconvert @echo \u0026#34;unconvert check\u0026#34; @GO111MODULE=on tools/bin/unconvert ./... 这就是我给 TiDB 添加用于检测无效类型转换的工具。\n7.revive\n# 编译到 tool/bin 目录下以供使用 tools/bin/revive: tools/check/go.mod cd tools/check; \\ $(GO) build -o ../bin/revive github.com/mgechev/revive # 先调用上面的编译命令，再调用进行检测 lint:tools/bin/revive @echo \u0026#34;linting\u0026#34; @tools/bin/revive -formatter friendly -config tools/check/revive.toml $(FILES) 该工具是一个代码风格格式化工具，可以看到我们定义了一个 revite.toml 规则文件来确定标准。在这里我们检测了所有文件。\n# revive.toml ignoreGeneratedHeader = false severity = \u0026#34;error\u0026#34; confidence = 0.8 errorCode = -1 warningCode = -1 [rule.blank-imports] [rule.context-as-argument] [rule.dot-imports] [rule.error-return] [rule.error-strings] [rule.error-naming] [rule.exported] [rule.if-return] [rule.var-naming] [rule.package-comments] [rule.range] [rule.receiver-naming] [rule.indent-error-flow] [rule.superfluous-else] [rule.modifies-parameter] [rule.unreachable-code] 8.evt\nvet: @echo \u0026#34;vet\u0026#34; $(GO) vet -all $(PACKAGES) 2\u0026gt;\u0026amp;1 | $(FAIL_ON_STDOUT) 我们也使用 Go 自带的检测工具来检测一些语法错误。\n9.staticcheck\n# 编译到 tool/bin 目录下以供使用 tools/bin/staticcheck: tools/check/go.mod cd tools/check; \\ $(GO) build -o ../bin/staticcheck honnef.co/go/tools/cmd/staticcheck # 先调用上面的编译命令，再调用进行检测 staticcheck:tools/bin/staticcheck @echo \u0026#34;static checking\u0026#34; @GO111MODULE=on tools/bin/staticcheck ./... 该工具主要检测一些无用代码和变量，并且会提供一些简化和优化代码的建议。\n10.tidy\ntidy: @echo \u0026#34;go mod tidy\u0026#34; ./tools/check/check-tidy.sh 我们也利用 Go 自带的 tidy 功能来防止依赖被污染，我们创建了一个脚本来检测依赖是否正常：\nset -euo pipefail # go mod tidy do not support symlink cd -P . cp go.sum /tmp/go.sum.before GO111MODULE=on go mod tidy diff -q go.sum /tmp/go.sum.before 该脚本会执行 tidy 命令并且和你原来的 sum 文件比较。 我编写 foo 函数时引入第三方库就是为了测试该脚本。\n最终的目录结构：\n. ├── foo.go ├── foo_test.go ├── go.mod ├── go.sum ├── LICENSE ├── main.go ├── Makefile ├── README.md └── tools ├── bin │ ├── errcheck │ ├── golangci-lint │ ├── gosec │ ├── goword │ ├── revive │ ├── staticcheck │ └── unconvert └── check ├── check-tidy.sh ├── errcheck_excludes.txt ├── go.mod ├── go.sum └── revive.toml 整合命令，快速检测 在上面我们定义好了编译和检测的命令，你可以在这里找到完整的 Makefile 文件。\n除了检查的命令，我们也可以定义一些常用的开发命令：\n# 构建 build: $(GOBUILD) # 清理 clean: $(GO) clean -i ./... rm -rf *.out # 测试 test: $(GOTEST) @\u0026gt;\u0026amp;2 echo \u0026#34;Great, all tests passed.\u0026#34; 现在我们所有的命令都就绪了，就可以开始封装整合命令：\ndev: check test check: fmt errcheck unconvert lint tidy check-static vet staticcheck goword 我们整合出了两个命令，一个是 check 它会执行所有的检测任务，另外一个是 dev 它不仅可以进行检查还跑了单元测试。我们在开发完成之后就可以进行检测并提交，甚至将其作为 CI 任务运行。\n到此为止，我们就基本完善了项目的静态检查工具链。该项目在 github 整理作为 template 项目 开源，大家可以直接使用 github template 功能初始化你的项目。 希望这篇文章对你集成静态代码分析工具有帮助！\n参考链接 tidb\n","permalink":"https://0xpoe.dev/posts/2020-04-25-%E4%BB%8E-pingcap-tidb-%E5%AD%A6%E4%B9%A0%E4%BD%BF%E7%94%A8%E9%9D%99%E6%80%81%E6%A3%80%E6%9F%A5%E5%B7%A5%E5%85%B7%E6%8F%90%E5%8D%87%E4%BB%A3%E7%A0%81%E8%B4%A8%E9%87%8F/","summary":"\u003cp\u003e大家好，我是 \u003ca href=\"https://github.com/0xPoe\"\u003eDongpo\u003c/a\u003e 。今天想跟大家简单介绍一下如何使用一些 golang 的静态代码检查工具来提升代码质量！\u003c/p\u003e\n\u003cp\u003e此博客在 \u003ca href=\"https://github.com/0xPoe/blog\"\u003eGitHub\u003c/a\u003e 上公开发布. 如果您有任何问题或疑问，请在此处打开一个 \u003ca href=\"https://github.com/0xPoe/blog/issues\"\u003eissue\u003c/a\u003e。\u003c/p\u003e\n\u003ch2 id=\"简介\"\u003e简介\u003c/h2\u003e\n\u003cp\u003e从去年接触到 TiDB 就开始尝试在社区帮忙修复一些简单的 Bug。最近，我在阅读代码的过程中发现 TiDB 的代码库中有大量的没有必要的类型转换，我就用 GoLand 分析检查出大部分的无效的类型转化，\n然后提了一个 \u003ca href=\"https://github.com/pingcap/tidb/pull/16262\"\u003ePR\u003c/a\u003e （CEO 半夜 review 代码，哈哈哈）修复。在这个 PR 中 \u003ca href=\"https://github.com/zz-jason\"\u003ezz-jason 大神\u003c/a\u003e 评论希望能够通过静态检查工具来检测无效的类型转换。\u003c/p\u003e\n\u003cp\u003e我经过一些研究，决定使用 \u003ca href=\"https://github.com/mdempsky/unconvert\"\u003eunconvert\u003c/a\u003e 来检测无效的类型转换，然后在这个 \u003ca href=\"https://github.com/pingcap/tidb/pull/16549\"\u003ePR\u003c/a\u003e 解决了这个问题。\n\u003cstrong\u003e最近我终于有机会在公司写 Go了，所以我也想在公司的项目上配置和使用一些静态检查工具来提升代码质量。\u003c/strong\u003e 在经过一下午的努力之后终于把 TiDB 的大部分检查工具移植到了公司项目上，并且在 github 上创建了一个模板项目\n\u003ca href=\"https://github.com/0xPoe/go-boilerplate\"\u003ego-boilerplate\u003c/a\u003e 。下面我就简单介绍一下这个模板的构建过程和使用的方式。\u003c/p\u003e\n\u003ch2 id=\"init-项目添加代码\"\u003einit 项目，添加代码\u003c/h2\u003e\n\u003cp\u003e我最近使用的 Go 版本 1.13.8，所以就使用 go mod 来初始化和管理项目。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e go mod init github.com/0xPoe/go-boilerplate\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e然后我添加了 main 文件和一个用作例子的 foo 文件，目录结构如下所示：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e├── foo.go\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e├── foo_test.go\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e├── go.mod\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e├── go.sum\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e├── LICENSE\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e├── main.go\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e├── Makefile\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e├── README.md\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e我在 main 文件中只是简单的输出一句话：\u003c/p\u003e","title":"从 pingcap tidb 学习使用静态检查工具提升代码质量"},{"content":"大家好，我是 Dongpo。最近在做 6.824 的实验 lab2，今天想写一篇文章记录一下前段时间做的 Raft 领导人选举机制的实现。\n此博客在 GitHub 上公开发布. 如果您有任何问题或疑问，请在此处打开一个 issue。\n简介 6.824 是 MIT 的分布式系统公开课，今年还有官方的视频资料放出。 目前也有国内的同学正在进行翻译工作，可以参考一下。\n课程质量非常高，国内也有一些整理好的相关资料可以参考。\n下面我就简单描述一下 Raft 领导人选举机制实现过程和一些踩过的坑，我会尽可能的聚焦在实现实验的细节上，因为我可能对 Raft 目前的理解也比较浅显。所以如果您对这篇文章感兴趣，请您先阅读论文和观看课程视频并尝试实现。\n领导人选举规则分析 在 Raft 中分别有三种角色分别是：Leader, Follower 和 Candidate。在整个系统正常运行过程中，只会有一个 Leader 节点，其他的都是 Follower 节点。只有在需要重新选举的阶段，节点会尝试将自己转换为 Candidate，然后向所有的节点发出投票 RPC 消息展开选举。\n我觉得而一个比较好的切入思路就是搞明白何时需要进行 Leader 的选举？我们阅读论文会发现主要有两种情况需要进行选举，一种就是当 Raft 集群启动之后开始第一次选举，另外一种就是 Leader 出现故障，无法使用心跳机制维持自己的统治， 导致选举超时机制触发，节点开始尝试新的一轮的选举(需要注意的是，选举有可能在一个 Term 中没有结果，那么就在下个 Term 中继续选举直到选出 Leader)。\n搞清楚何时进行选举，我们再来看看选举的 RPC 请求和回复的数据结构：\nRequest 参数 解释 term 候选人的任期号 candidateId 请求选票的候选人的 Id lastLogIndex 候选人的最后日志条目的索引值 lastLogTerm 候选人最后日志条目的任期号 Reply 返回值 解释 term 当前任期号，以便于候选人去更新自己的任期号 voteGranted 候选人赢得了此张选票时为真 从数据结构中我们可以看到能够影响到选举的主要有两个部分，一个是 Term，另外一个就是候选人的最后一条日志条目。那我们就尝试从这两点方面去消化和理解选举的规则：\nTerm 候选人的 Term 必须大于投票者的 Term，否则就立马拒绝投票。 候选人最后一条日志 候选人的最后一条日志的 Term 要大于投票者的最后一条日志的 Term（保证日志最新） 或者当它们的最后一条日志的 Term 相同时，候选人最后一条日志的 Index 要大于等于投票者（保证日志最长）。 主要的规则就是这两条，第一条还是很好理解的，我们不应该投票给 Term 落后的节点。日志这一条规则出自论文 5.4.1 领导选举限制，我们必须保证我们选出的领导拥有系统中绝大部分节点承认的最新最长的日志记录。 所幸这个规则是为了保证日志的安全性，对我们实现实验的 A 部分还不影响。如果你对这条规则的设置很迷惑，可以先继续实验，等到做完日志复制部分实验之后，可能就会对这个规则有更深更清晰的理解。\n当我们成为领导人之后需要通过心跳机制来维持自己的统治，**也就是不停的发空日志给 Follower，然后通过这种方式是刷新该 Follower 的选举超时时间，让它不要发起选举。**所以在开始实验之前我们还需要简单看一下附加日志的 PRC 请求和回复的数据结构：\n参数 解释 term 领导人的任期号 leaderId 领导人的 Id，以便于跟随者重定向请求 prevLogIndex 新的日志条目紧随之前的索引值 prevLogTerm prevLogIndex 条目的任期号 entries[] 准备存储的日志条目（表示心跳时为空；一次性发送多个是为了提高效率） leaderCommit 领导人已经提交的日志的索引值 返回值 解释 term 当前的任期号，用于领导人去更新自己 success 跟随者包含了匹配上 prevLogIndex 和 prevLogTerm 的日志时为真 这些数据结构的大多数字段都是为了附加日志设计的，所以我们可以尽量的屏蔽掉无用的字段，目前对实验 A 有影响的就只有 Term 字段，所以我们在接下来的实现中确保 Term 处理逻辑正确即可。\n测试用例分析 在开始实现之前我们先来看看实验的测试用例。领导人实验实际上很简单，所有它的测试用例也很简单，主要有两个测试：\nTestInitialElection2A: 简单测试是否可以完成正常的领导人选举 TestReElection2A: 测试当第一次选出的领导人故障之后，是否能正常选举出新的领导人。 这些测试用例都很简单，其中测试用例用到一个函数：\n// 检查是否确实仅存在一个领导人。 // 为了测试重新选举，所以会多尝试几次查找。 func (cfg *config) checkOneLeader() int { // 多迭代几次尝试寻找领导人。 for iters := 0; iters \u0026lt; 10; iters++ { ms := 450 + (rand.Int63() % 100) // 随机尝试。 time.Sleep(time.Duration(ms) * time.Millisecond) // 获取领导人和对应上任 Term。 leaders := make(map[int][]int) for i := 0; i \u0026lt; cfg.n; i++ { if cfg.connected[i] { if term, leader := cfg.rafts[i].GetState(); leader { leaders[term] = append(leaders[term], i) } } } // 校验没有在一个周期内选出两个领导人，找到最后新的Term。 lastTermWithLeader := -1 for term, leaders := range leaders { if len(leaders) \u0026gt; 1 { cfg.t.Fatalf(\u0026#34;term %d has %d (\u0026gt;1) leaders\u0026#34;, term, len(leaders)) } if term \u0026gt; lastTermWithLeader { lastTermWithLeader = term } } // 找到最新的领导人。 if len(leaders) != 0 { return leaders[lastTermWithLeader][0] } } cfg.t.Fatalf(\u0026#34;expected one leader, got none\u0026#34;) return -1 } 启动 go 程，开始选举 我们读完测试用例之后，就可以尝试开始实现领导人选举。我们需要在 Make 函数中为该节点启动一个后台常驻 go 程，在里面发起选举。\nfunc Make(peers []*labrpc.ClientEnd, me int, persister *Persister, applyCh chan ApplyMsg) *Raft { rf := \u0026amp;Raft{} rf.peers = peers rf.persister = persister rf.me = me // 代表无领导人。 rf.leaderId = -1 rf.currentTerm = 0 // 代表还未投票。 rf.votedFor = -1 rf.commitIndex = 0 rf.lastAppliedIndex = 0 rf.state = Follower rf.lastReceiveTime = time.Now() // 开始选举。 go rf.startLeaderElection() rf.readPersist(persister.ReadRaftState()) return rf } // 下面附带一些会用到的一些角色转换的函数。 func (rf *Raft) convertToCandidate() { rf.state = Candidate rf.currentTerm++ rf.votedFor = rf.me rf.lastReceiveTime = time.Now() } func (rf *Raft) convertToFollower(newTerm int) { rf.state = Follower rf.currentTerm = newTerm rf.votedFor = -1 rf.lastReceiveTime = time.Now() } func (rf *Raft) convertToLeader() { rf.state = Leader rf.leaderId = rf.me rf.lastReceiveTime = time.Now() } 在 Make 函数中，我初始化了节点的基本信息，需要注意的是我为每个节点设置了一个叫做 lastReceiveTime 的字段，这个字段可以用作表示上次收到有效的 RPC 附加日志（在试验 A 中用作心跳）的时间， 当我发现我们接受到有效心跳的时间到现在超过了选举超时时间限制时，我们就将其转换为候选人发起一轮新的选举。\n关于选举我选择直接在我们的选举 go 程中开 for 循环去执行，没有再使用额外的 go 程和 channel 来处理选举超时机制，因为我觉得对我来说那样可能会有滥用 channel 的嫌疑，造成不必要的心智负担：\n// 开始选举领导人。 // 当发生一次选举超时时，节点就开始一轮新的选举。 func (rf *Raft) startLeaderElection() { for { electionTimeout := rand.Intn(150) startTime := time.Now() time.Sleep(time.Duration(HeartbeatInterval+electionTimeout) * time.Millisecond) rf.mu.Lock() if atomic.LoadInt32(\u0026amp;rf.dead) == Dead { rf.mu.Unlock() return } // 在这个地方判断是否应该发起选举。 if rf.lastReceiveTime.Before(startTime) { if rf.state != Leader { DPrintf(\u0026#34;%d kicks off election on term: %d\u0026#34;, rf.me, rf.currentTerm) go rf.kickOffElection() } } rf.mu.Unlock() } } 我们可以看到真实的选举实际上被我放在了 kickOffElection 中：\n// 触发选举，向所有节点发出投票请求。 func (rf *Raft) kickOffElection() { rf.mu.Lock() // 将自己转换为候选人。 rf.convertToCandidate() lastLogEntry := rf.getLastLogEntry() args := RequestVoteArgs{ Term: rf.currentTerm, CandidateId: rf.me, LastLogIndex: lastLogEntry.Index, LastLogTerm: lastLogEntry.Term, } numVote := 1 rf.mu.Unlock() // 开始向每个节点发送投票请求。 for i := 0; i \u0026lt; len(rf.peers); i++ { if i != rf.me { go func(peerId int) { reply := RequestVoteReply{} DPrintf(\u0026#34;%d send vote request to %d\u0026#34;, rf.me, peerId) // 发出 RPC 消息，注意消息和回复字段要求大写。 ok := rf.sendRequestVoteRPC(peerId, \u0026amp;args, \u0026amp;reply) if !ok { return } rf.mu.Lock() defer rf.mu.Unlock() // 如果投票人的 Term 比当前节点的 Term 大，那将节点转换为 Follower。 if reply.Term \u0026gt; rf.currentTerm { rf.convertToFollower(reply.Term) return } // 收到了有效投票，统计结果，如果获取系统中一半以上的选票，就将该节点转换为领导人，并且开始放发出心跳维持自己的统治。 if reply.VoteGranted { numVote++ if numVote \u0026gt; len(rf.peers)/2 \u0026amp;\u0026amp; rf.state == Candidate { rf.convertToLeader() DPrintf(\u0026#34;%d become the leader on term: %d\u0026#34;, rf.me, rf.currentTerm) for j := 0; j \u0026lt; len(rf.peers); j++ { if j != rf.me { go rf.replicaLogToPeer(j) } } } } }(i) } } } 我们来简单梳理一下这部分逻辑：\n1.先将自己的角色转换为候选人，并且在请求消息中带上自己的当前 Term 和 自身编号。 2.向每个节点发出投票请求（需要注意在实验中的 RPC 的消息和回复数据结构字段都要大写，否则会发送 RPC 失败）。 3.收到回复，如果投票人的 Term 比该节点大，那就将改节点转换为 Follower 结束选举。如果成功获得选票，就判断是否已经收到足够多的选票，节点转换为领导人，并且启动一个 go 程开始发送心跳维持统治（go rf.replicaLogToPeer(j)）。 在开始查看如何发送心跳之前，我们先来看看作为其他节点我们应该如何依照上面分析的规则进行投票，我们的投票 RPC 都是交给了 RequestVote 函数去处理：\n// // 投票请求 RPC 处理. // func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) { rf.mu.Lock() defer rf.mu.Unlock() DPrintf(\u0026#34;%d start process vote request\u0026#34;, rf.me) // 设置回复消息的 Term。 reply.Term = rf.currentTerm // 对应我上面说的关于 Term 的那一条规则。 if args.Term \u0026lt; rf.currentTerm { reply.VoteGranted = false DPrintf(\u0026#34;%d refuse vote for %d\u0026#34;, rf.me, args.CandidateId) } else { // 如果候选人的 Term 比我们的大，那我们应该将自己转换为 Follower。 if args.Term \u0026gt; rf.currentTerm { rf.convertToFollower(args.Term) } lastEntry := rf.getLastLogEntry() // 对应我上面说的关于日志的那一条规则。 logUpToDate := LogEntry{Command: nil, Index: args.LastLogIndex, Term: args.LastLogTerm}.isMoreUpToDate(lastEntry) if rf.votedFor == -1 \u0026amp;\u0026amp; logUpToDate { rf.votedFor = args.CandidateId reply.VoteGranted = true DPrintf(\u0026#34;%d vote for %d\u0026#34;, rf.me, args.CandidateId) } } } // 是否更新更长. func (l LogEntry) isMoreUpToDate(r LogEntry) bool { return (l.Term == r.Term \u0026amp;\u0026amp; l.Index \u0026gt;= r.Index) || l.Term \u0026gt; r.Term } 在上面的 RequestVote 函数中，我们主要就是根据分析中描述的规则进行投票。接下来我们看看当节点顺利成为领导人之后如何用心跳来维持自己的统治。领导人需要正确的发送心跳，不停的刷新 Follower 的 lastReceiveTime：\n// 发送日志到附加节点，还是采用 for 循环的形式。 func (rf *Raft) replicaLogToPeer(peerId int) { DPrintf(\u0026#34;%d start sync log to %d\u0026#34;, rf.me, peerId) for { rf.mu.Lock() if rf.state != Leader { rf.mu.Unlock() DPrintf(\u0026#34;%d stop sends append entries to %d\u0026#34;, rf.me, peerId) return } rf.mu.Unlock() go rf.sendAppendEntry(peerId) // 设置一定心跳间隔。 time.Sleep(HeartbeatInterval * time.Millisecond) } } // 发送日志给节点. func (rf *Raft) sendAppendEntry(peerId int) { rf.mu.Lock() lastLogEntry := rf.getLastLogEntry() args := AppendEntriesArgs{ Term: rf.currentTerm, // 我们需要填好这个值，在其他节点处理该请求的时候会用到这个值。 LeaderId: rf.me, // 设置自己为的编号。 PrevLogIndex: lastLogEntry.Index, // 这个值对我们目前实验无影响。 Entries: nil, // 表示心跳。 LeaderCommit: -1, // 这个值对我们目前实验无影响，瞎填就可以. } reply := AppendEntriesReply{} rf.mu.Unlock() // 发送日志 RPC。 ok := rf.sendAppendEntryRPC(peerId, \u0026amp;args, \u0026amp;reply) if !ok { log.Printf(\u0026#34;%d send a append PRC to %d failed\u0026#34;, rf.me, peerId) return } rf.mu.Lock() defer rf.mu.Unlock() // 如果发现回复的 Term 比我们自己的高，我们就将自己转换为 Follower。 if reply.Term \u0026gt; rf.currentTerm { rf.convertToFollower(reply.Term) return } log.Printf(\u0026#34;%d success send a append to %d\u0026#34;, rf.me, peerId) } 上面的函数 replicaLogToPeer 和 sendAppendEntry 完成心跳 RPC 不停发送和回复处理。我们再来看一看其他节点收到心跳之后该如何处理：\n// // 附加日志 RPC 处理. // func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) { rf.mu.Lock() defer rf.mu.Unlock() log.Printf(\u0026#34;%d start process append entries request from %d, args term: %d node term: %d\u0026#34;, rf.me, args.LeaderId, args.Term, rf.currentTerm) // 如果 Term 比改节点的小，那我们就认为这是一条无效的心跳，直接返回。 if args.Term \u0026lt; rf.currentTerm { DPrintf(\u0026#34;get append entries form %d, but the term less than %d\u0026#34;, args.LeaderId, rf.me) reply.Term, reply.Success = rf.currentTerm, false return } // 刷新该节点的 lastReceiveTime. reply.Term = rf.currentTerm rf.leaderId = args.LeaderId rf.lastReceiveTime = time.Now() if args.Term \u0026gt; rf.currentTerm { DPrintf(\u0026#34;%d convert itself as follower\u0026#34;, rf.me) rf.convertToFollower(args.Term) } } 上面的 AppendEntries 函数主要的作用就是去刷新 Follower 的最近一次接受到有效心跳的时间，阻止该节点发生选举超时，从而维持自己的统治。\n到此为止，我们就实现完了一个简单的领导人选举，思路其实还是很清晰，主要就是完全遵照论文的数据结构和规则来按部就班的实现。由于课程要求不能公开源码，所以我就不提供实现链接啦， 希望这篇文章能帮助你梳理和理解实验 A 的实现思路。最近我也在尝试实现 Rust 版本的 Raft，到时候再来跟大家分享交流源码吧😁\n踩坑记录 看上去领导人选举的思路非常简单，但是我在实现的过程中也踩了不少的坑：\n注意锁的获取和释放，因为到处都在获取和释放，有可能会导致因为拿锁不当导致死锁（我有一天写出了 defer rf.mu.Lock 这样的代码）。 心跳间隔不要设置大大，否则就会偶尔出现领导人正常但是触发选举的情况（测试用例会警告）。 调试需要多打 log，不然根本都不知道到底什么地方出了问题。 测试要多跑几遍来验证结果，推荐一个来自大神 Jon Gjengset 的测试脚本。 参考链接 MIT 6.824 2020 Raft 实现细节汇总\nRaft 课程视频\n6.824 社区实现\nRaft 论文中文翻译\n","permalink":"https://0xpoe.dev/posts/2020-04-12-raft-%E9%A2%86%E5%AF%BC%E4%BA%BA%E9%80%89%E4%B8%BE%E5%AE%9E%E7%8E%B0/","summary":"\u003cp\u003e大家好，我是 \u003ca href=\"https://github.com/0xPoe\"\u003eDongpo\u003c/a\u003e。最近在做 6.824 的实验 lab2，今天想写一篇文章记录一下前段时间做的 Raft 领导人选举机制的实现。\u003c/p\u003e\n\u003cp\u003e此博客在 \u003ca href=\"https://github.com/0xPoe/blog\"\u003eGitHub\u003c/a\u003e 上公开发布. 如果您有任何问题或疑问，请在此处打开一个 \u003ca href=\"https://github.com/0xPoe/blog/issues\"\u003eissue\u003c/a\u003e。\u003c/p\u003e\n\u003ch2 id=\"简介\"\u003e简介\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"http://nil.csail.mit.edu/6.824/2020/schedule.html\"\u003e6.824\u003c/a\u003e 是 MIT 的分布式系统公开课，今年还有官方的\u003ca href=\"https://www.youtube.com/channel/UC_7WrbZTCODu1o_kfUMq88g\"\u003e视频资料\u003c/a\u003e放出。\n目前也有国内的同学正在进行\u003ca href=\"https://github.com/ivanallen/thor\"\u003e翻译工作\u003c/a\u003e，可以参考一下。\u003c/p\u003e\n\u003cp\u003e课程质量非常高，国内也有一些整理好的\u003ca href=\"https://github.com/chaozh/MIT-6.824\"\u003e相关资料\u003c/a\u003e可以参考。\u003c/p\u003e\n\u003cp\u003e下面我就简单描述一下 Raft 领导人选举机制实现过程和一些踩过的坑，我会尽可能的聚焦在实现实验的细节上，因为我可能对 Raft 目前的理解也比较浅显。\u003cstrong\u003e所以如果您对这篇文章感兴趣，请您先阅读论文和观看课程视频并尝试实现。\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"领导人选举规则分析\"\u003e领导人选举规则分析\u003c/h2\u003e\n\u003cp\u003e在 Raft 中分别有三种角色分别是：Leader, Follower 和 Candidate。在整个系统正常运行过程中，只会有一个 Leader 节点，其他的都是 Follower 节点。只有在需要重新选举的阶段，节点会尝试将自己转换为\nCandidate，然后向所有的节点发出投票 RPC 消息展开选举。\u003c/p\u003e\n\u003cp\u003e我觉得而一个比较好的切入思路就是搞明白何时需要进行 Leader 的选举？我们阅读论文会发现主要有两种情况需要进行选举，\u003cstrong\u003e一种就是当 Raft 集群启动之后开始第一次选举，另外一种就是 Leader 出现故障，无法使用心跳机制维持自己的统治，\n导致选举超时机制触发，节点开始尝试新的一轮的选举(需要注意的是，选举有可能在一个 Term 中没有结果，那么就在下个 Term 中继续选举直到选出 Leader)。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e搞清楚何时进行选举，我们再来看看选举的 RPC 请求和回复的数据结构：\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003eRequest 参数\u003c/th\u003e\n          \u003cth\u003e解释\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eterm\u003c/td\u003e\n          \u003ctd\u003e候选人的任期号\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003ecandidateId\u003c/td\u003e\n          \u003ctd\u003e请求选票的候选人的 Id\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003elastLogIndex\u003c/td\u003e\n          \u003ctd\u003e候选人的最后日志条目的索引值\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003elastLogTerm\u003c/td\u003e\n          \u003ctd\u003e候选人最后日志条目的任期号\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cbr\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003eReply 返回值\u003c/th\u003e\n          \u003cth\u003e解释\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eterm\u003c/td\u003e\n          \u003ctd\u003e当前任期号，以便于候选人去更新自己的任期号\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003evoteGranted\u003c/td\u003e\n          \u003ctd\u003e候选人赢得了此张选票时为真\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cbr\u003e\n\u003cp\u003e\u003cstrong\u003e从数据结构中我们可以看到能够影响到选举的主要有两个部分，一个是 Term，另外一个就是候选人的最后一条日志条目。那我们就尝试从这两点方面去消化和理解选举的规则：\u003c/strong\u003e\u003c/p\u003e","title":"Raft 领导人选举实现"},{"content":"大家好，我是 Dongpo。今天想跟大家介绍下如何快速的成为 antd 的贡献者，希望能够对想要加入 antd 社区贡献的同学有帮助！\n此博客在 GitHub 上公开发布. 如果您有任何问题或疑问，请在此处打开一个 issue。\n简介 其实 antd 几乎不需要做任何的介绍，大家都知道它是中国甚至世界都炙手可热的 UI 设计框架。他们的口号是：企业级产品设计体系，创造高效愉悦的工作体验。\n我对这个框架感情颇深，从大二开始接触到这个框架至今已经陪我走过了 3 个年头，在校和实习期间用它愉悦的完成了数不清的作业和任务。去年在毕业设计期间，终于为它贡献了第一份力量。\n最近我还在为它持续贡献力量，所以萌生了写一篇有关如何给 antd 贡献的文章的念头。言归正传，下面我会用我前两天帮 antd 修复一个 Bug 的例子来带大家熟悉一下在 antd 社区贡献的简单流程。\n熟悉 PR 流程，阅读贡献指南 在我开始介绍我的例子之前，我觉得如果你刚开始尝试在 Github 的社区开始贡献，建议你读一下 Github 官方的文档。\n官方的文档介绍了一些很基础但是很重要的关于 PR 的规则，当你熟悉如何在 Github 创建一个 PR 之后，你还应该在 antd 的 README 中找到贡献指南，README 是每个开源项目很重要的入口， 你基本上在 README 中可以找到所有与该库相关的链接。\n对于 antd 来说，他们把自己贡献指南挂在自己的官网当中，既有中文版也有英文版。你可按照自己的情况仔细阅读完贡献指南。 我觉得目前我们可以从贡献指南获取的要点信息是：\n开发准则\n对于开发准则来说，最主要的我觉得就是要保持尊重和礼貌，无论是对贡献者还是社区协作者，我们都应该保持绝对的尊重和礼貌。 一个要点就是在和社区交流的时候多用：please, could, thanks，这在哪个社区都适用。 分支管理\n这个是一个很重要的点，因为每个项目可能都有自己的开发和发布流程，在 antd 我们要注意的是：修复 Bug 需要将 PR 发送到 master 分支，添加新功能则需要将 PR 发送到 feature 分支。 提交 Bug 报告\n其实在开源社区提交 Bug 报告也是开源很重要的一环，所以大家不要觉得只有提 PR 才算贡献，其实只要是能促进代码库变得更好的事情在我看来都是贡献。 需要注意的是，我们在给 antd 提交 issue 报告时不能直接去仓库的 issue 位置开启一个报告，我们需要在 antd 的 issue 小助手 上面提交问题， 为什么我们需要这样做呢？因为大型的开源项目，可能都会收到上百成千的 issue，但是这些 issue 提的风格迥异，会对社区协作者造成很大的困扰，用户和社区互相折磨。 关于如何创建一个好的 Bug 的报告，可以看看这两篇文章：How-To-Ask-Questions-The-Smart-Way 和 如何有效地报告 Bug。 基本上我们得到这些要素信息之后，我们就可以开始尝试去给社区做一些简单的贡献，下面我就通过一个例子给大家演示下一个 PR 的合并过程。\n挑选问题，开始挑战 我们的 PR 一般都是去修复 Bug 或者去添加一个新的功能，当我们刚开始参与一个开源项目的时候，我们可以从 Good First Issue 开始，大多数的开源项目都会给一些简单的问题加上 good first issue 的标签， antd 就有这样被标记的问题，但是一般的活跃社区这样的问题都比较抢手， 可能问题刚发出来就被别人领取之后开始修复了，但是除了这些问题之外，在 antd 还有一类问题我觉得难度适中，可以作为一些入门的问题来尝试。\n在 antd 的 issue 中，有一类问题被 help wanted 的标签标记，这些问题大多都是一些用户提的需求或者简单的 Bug，我们可以从这些问题入手，比如我上周六在 issue 列表中，发现了这个 help wanted 的问题。\n{:height=\u0026ldquo;550px\u0026rdquo; width=\u0026ldquo;750px\u0026rdquo;}\n我看到这个问题的时候，报告人已经做了一些研究，似乎是 referer HTTP 头信息造成的。一般在开源社区中，如果你想修复这个问题，你就可以在下方留言自己已经在尝试修复这个问题或者 @ 社区协作者 询问看你能不能尝试修复这个问题。因为我没看到别人的留言，我就开始了一些尝试去修复这个问题。\n尝试修复，提交 PR 首先，我没有在自己的网站或者公司的网站中使用过 codesandbox，所以集成 codesandbox 对我来说是个陌生的事情。 但是能够发现问题报告人似乎已经做出了研究，那我能不能顺着提问者的思路去解决这个问题呢？\n关于 referer 你可以查阅它的一些标准，你会发现它还有个历史遗留问题。referer 实际上是 \u0026ldquo;referrer\u0026rdquo; 误拼写。 看完之后我还看到了阮一峰老师的博客，但是实际上文章中有点小错误，我已经留言了，可能阮一峰老师还没来得及看。\n关于 referer 的相关信息我就不再赘述，可以查看上面的链接文章。当时我想能不能就简单暴力的设置 \u0026lt;meta name=\u0026quot;referrer\u0026quot; content=\u0026quot;no-referrer\u0026quot;\u0026gt; 来解决呢？它表示我们对于任何请求都不带 referrer 信息。我在本地测试发现确实有效。\n因为我觉得也许这个功能对于 antd 来说本身没有什么影响，所以我就提交了这份代码。 在 antd 中提交 PR，跟提交问题报告一样也需要遵循一份 PR 的提交模板，这份模板是放在 .github 目录下的, 它也有中文版，所以我就不再描述里面的内容，因为我觉得还是比较清晰的。 我的 PR 是修复 Bug，所以根据我们从贡献指南中获取到的信息来说，我们应该创建一个 PR 到 master 分支并填好 PR 的相关信息：\n{:height=\u0026ldquo;550px\u0026rdquo; width=\u0026ldquo;800px\u0026rdquo;}\n到这里，似乎大功告成，坐等 PR 被合并了(我在 PR 的信息中使用了 Github 关联 PR 和 issue 的功能 \u0026ldquo;Fixes #22636\u0026rdquo;，查看用法)。\n收到建议，重新修改 但是实际上没有那么顺利，一般我们在提交 PR 之后，大概率会收到社区协作者的 code review，在 antd 中，会有🤖来帮你分配 reviewer。我的 PR 被 afc163 大神 review 了，收到了如下的评论：\n{:height=\u0026ldquo;450px\u0026rdquo; width=\u0026ldquo;750px\u0026rdquo;}\n在和 afc163 交流之后，我们确定需要只对 codesandbox 做特殊的处理，所以我就开始了一次新的尝试。\n首先，从思路上来说，目前想要做这种特殊的处理，我们只能对使用特殊的标签来处理这种情况，具体支持单独设置 referer 的标签可以查阅这个文档。 同时我还去查阅了 codesandbox 的官方文档，我发现他们提供的定义接口其实不光支持 post 请求，同时还支持 get 请求:\n{:height=\u0026ldquo;450px\u0026rdquo; width=\u0026ldquo;750px\u0026rdquo;}\n那我立刻有了思路，我可以直接使用 \u0026lt;a\u0026gt; 标签来发送 get 请求解决这个问题，我立刻修改代码发送了 get 请求并且设置了 referer 规则：\n\u0026lt;form\u0026gt; \u0026lt;Tooltip title={\u0026lt;FormattedMessage id=\u0026#34;app.demo.codesandbox\u0026#34; /\u0026gt;}\u0026gt; {/* FIXME: This temporarily solution to fix issues/22636, we need revert it to use form after the codesandbox api issue fixed. */} \u0026lt;a aria-label=\u0026#34;Create New Sandbox with Prefilled Data\u0026#34; // {/* 这个属性详见 https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA*/} rel=\u0026#34;noopener noreferrer\u0026#34; // {/* 设置不带 referrer 信息 */} target=\u0026#34;_blank\u0026#34; href={`https://codesandbox.io/api/v1/sandboxes/define?parameters=${compress( JSON.stringify(codesanboxPrefillConfig), )}`} {/* 生成参数 */} className=\u0026#34;code-box-codesandbox\u0026#34; onClick={() =\u0026gt; this.track({ type: \u0026#39;codesandbox\u0026#39;, demo: meta.id })} /\u0026gt; \u0026lt;/Tooltip\u0026gt; \u0026lt;/form\u0026gt; 我提交这段代码，然后顺利的通过了 afc163 的 review，然后代码就可以顺利的合进了 master 分支了，我们关联的问题 Github 也会自动的关闭。\n但是这样确实就真的解决问题了吗？但是我到这一步还是没能找到问题所在，为什么会像问题里面说的突然这几天就出问题呢？\n排查问题，继续跟踪 我好奇的是为什么这个东西会突然出问题呢？然后我就去翻了 master 分支的提交记录，发现并没有最近的跟它相关的提交修改记录，所以我就去了 codesandbox 官方文档又看了看，然后找到了一个他们官方的 demo。 我发现他们的 demo 也是需要重定向的，所以我觉得这应该不是 antd 的修改造成的问题。然后我又在官网上发现了他们的 codesandbox-client 开源仓库。\n找到三方依赖的仓库，我们就可以提交一个 issue 去让他们帮忙查查看，另外也可以看看别人有没有这个问题，所以我创建了这个 issue 来继续跟踪这个问题。\n终于在三天之后，我收到了该社区的回复，我们发现其他人也有和 antd 类似的问题。该社区的协作者迅速的修复了这个问题。 他们解释说这是他们最近后端的一个跨域修改造成的，但是没有说明具体的细节，然后迅速的解决了这个问题。\n回滚代码，解决问题 当 codesandbox 的问题修复之后，我也按照上面的流程再次发起了一个 PR 去回滚了我的代码，因为可以看到我在上面的代码中加入了一个 FIXME，我的那种修改只是一种暂时的解决方案，因为我的修改方案会与其他几个三方的链接模式不一致， 所以我在这个 PR 中回滚了代码。\n到此为止，我们就真正意义上的解决了这个问题，我们也完整的走完了一个给 antd 贡献的流程。希望我的这篇记录能对你尝试在 antd 社区贡献有帮助！\n此外我还在 antd 开了个跟骨架屏相关的坑，欢迎大家来 antd 一起填坑！\n参考链接 antd contributing guide\nrust contributing guide\ngithub docs\n","permalink":"https://0xpoe.dev/posts/2020-04-02-%E5%A6%82%E4%BD%95%E5%BF%AB%E9%80%9F%E7%9A%84%E6%88%90%E4%B8%BA-ant-design-%E7%9A%84-contributor/","summary":"\u003cp\u003e大家好，我是 Dongpo。今天想跟大家介绍下如何快速的成为 antd 的贡献者，希望能够对想要加入 antd 社区贡献的同学有帮助！\u003c/p\u003e\n\u003cp\u003e此博客在 \u003ca href=\"https://github.com/0xPoe/blog\"\u003eGitHub\u003c/a\u003e 上公开发布. 如果您有任何问题或疑问，请在此处打开一个 \u003ca href=\"https://github.com/0xPoe/blog/issues\"\u003eissue\u003c/a\u003e。\u003c/p\u003e\n\u003ch2 id=\"简介\"\u003e简介\u003c/h2\u003e\n\u003cp\u003e其实 \u003ca href=\"https://github.com/ant-design/ant-design/\"\u003eantd\u003c/a\u003e 几乎不需要做任何的介绍，大家都知道它是中国甚至世界都炙手可热的 UI 设计框架。他们的口号是：\u003cstrong\u003e企业级产品设计体系，创造高效愉悦的工作体验。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e我对这个框架感情颇深，从大二开始接触到这个框架至今已经陪我走过了 3 个年头，在校和实习期间用它愉悦的完成了数不清的作业和任务。去年在毕业设计期间，\u003ca href=\"https://github.com/ant-design/ant-design/pull/16398\"\u003e终于为它贡献了第一份力量\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e最近我还在为它持续贡献力量，所以萌生了写一篇有关如何给 antd 贡献的文章的念头。言归正传，下面我会用我前两天帮 antd 修复一个 Bug 的例子来带大家熟悉一下在 antd 社区贡献的简单流程。\u003c/p\u003e\n\u003ch2 id=\"熟悉-pr-流程阅读贡献指南\"\u003e熟悉 PR 流程，阅读贡献指南\u003c/h2\u003e\n\u003cp\u003e在我开始介绍我的例子之前，我觉得如果你刚开始尝试在 Github 的社区开始贡献，建议你读一下 \u003ca href=\"https://help.github.com/cn/github/collaborating-with-issues-and-pull-requests/about-pull-requests\"\u003eGithub 官方的文档\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e官方的文档介绍了一些很基础但是很重要的关于 PR 的规则，当你熟悉如何在 Github 创建一个 PR 之后，你还应该在 \u003ca href=\"https://github.com/ant-design/ant-design\"\u003eantd\u003c/a\u003e 的 README 中找到贡献指南，README 是每个开源项目很重要的入口，\n你基本上在 README 中可以找到所有与该库相关的链接。\u003c/p\u003e\n\u003cp\u003e对于 antd 来说，他们把自己贡献指南挂在自己的官网当中，既有\u003ca href=\"https://ant.design/docs/react/contributing-cn\"\u003e中文版\u003c/a\u003e也有\u003ca href=\"https://ant.design/docs/react/contributing\"\u003e英文版\u003c/a\u003e。你可按照自己的情况仔细阅读完贡献指南。\n我觉得目前我们可以从贡献指南获取的要点信息是：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/ant-design/ant-design/blob/master/CODE_OF_CONDUCT.md\"\u003e开发准则\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e对于开发准则来说，最主要的我觉得就是要保持尊重和礼貌，无论是对贡献者还是社区协作者，我们都应该保持绝对的尊重和礼貌。\u003c/li\u003e\n\u003cli\u003e一个要点就是在和社区交流的时候多用：please, could, thanks，这在哪个社区都适用。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e分支管理\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e这个是一个很重要的点，因为每个项目可能都有自己的开发和发布流程，在 antd 我们要注意的是：\u003cstrong\u003e修复 Bug 需要将 PR 发送到 master 分支，添加新功能则需要将 PR 发送到 feature 分支。\u003c/strong\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e提交 Bug 报告\u003c/p\u003e","title":"如何快速的成为 Ant Design 的 contributor"},{"content":"最近在学习 Rust，并且在尝试使用 Rust 来做内核，跟着 Writing an OS in Rust{:target=\u0026quot;_blank\u0026quot;} 系列博客学习，这个博客质量非常高，深入浅出。\n但是最近遇到一个qemu的问题,当我将系统升级到最近的 macOS 10.15 Catalina 之后，qemu 无法运行，启动之后卡死。如下： {:height=\u0026ldquo;360px\u0026rdquo; width=\u0026ldquo;450px\u0026rdquo;}\n解决方案 实际上卡死的原因是 Catalina 操作系统底层 API 变化导致的死锁。该死锁会导致 qemu 的 UI Cocoa 无法正常工作。\n所以我们目前运行 qemu 的非 GUI 模式是正常的。\n后来我在 qemu 的论坛发现有人报告这个 bug，并且有人已经修复了这个问题，但是还没有 release。但是也有人给出了其他解决方案，可以通过自己编译切换 UI 来解决这个问题。\n步骤如下（我使用homebrew安装，其他安装方式也一样，重新编译就行）： brew edit qemu 添加依赖：sdl2，depends_on \u0026quot;sdl2\u0026quot; 关闭 cocoa，将--enable-cocoa 改为 --disable-cocoa 打开 sdl，将--disable-sdl 改为 --enable-sdl brew reinstall --build-from-source qemu 最终的 Formula 文件：\nclass Qemu \u0026lt; Formula desc \u0026#34;x86 and PowerPC Emulator\u0026#34; homepage \u0026#34;https://www.qemu.org/\u0026#34; url \u0026#34;https://download.qemu.org/qemu-4.1.0.tar.xz\u0026#34; sha256 \u0026#34;656e60218689bdeec69903087fd7582d5d3e72238d02f4481d8dc6d79fd909c6\u0026#34; head \u0026#34;https://git.qemu.org/git/qemu.git\u0026#34; bottle do sha256 \u0026#34;b8e20707ff317f4182fbd180f033f4a9f163e40fec54a442d9b227e2f39846db\u0026#34; =\u0026gt; :catalina sha256 \u0026#34;3b35079b1729b9b1bd58087794ccd9d40848b69d22a601c12b451791709298d6\u0026#34; =\u0026gt; :mojave sha256 \u0026#34;0ef458cc2c387f8bbb2bdbd0e4d6e2a21574eb735c1621ad8e0cd3e094288298\u0026#34; =\u0026gt; :high_sierra sha256 \u0026#34;68d05c70584e722c8109682ddc041a6271e0c36d102c7d3f55553ba3b4f39480\u0026#34; =\u0026gt; :sierra end depends_on \u0026#34;libtool\u0026#34; =\u0026gt; :build depends_on \u0026#34;pkg-config\u0026#34; =\u0026gt; :build depends_on \u0026#34;glib\u0026#34; depends_on \u0026#34;gnutls\u0026#34; depends_on \u0026#34;jpeg\u0026#34; depends_on \u0026#34;libpng\u0026#34; depends_on \u0026#34;libssh\u0026#34; depends_on \u0026#34;libusb\u0026#34; depends_on \u0026#34;lzo\u0026#34; depends_on \u0026#34;ncurses\u0026#34; depends_on \u0026#34;pixman\u0026#34; depends_on \u0026#34;vde\u0026#34; depends_on \u0026#34;sdl2\u0026#34; # 820KB floppy disk image file of FreeDOS 1.2, used to test QEMU resource \u0026#34;test-image\u0026#34; do url \u0026#34;https://dl.bintray.com/homebrew/mirror/FD12FLOPPY.zip\u0026#34; sha256 \u0026#34;81237c7b42dc0ffc8b32a2f5734e3480a3f9a470c50c14a9c4576a2561a35807\u0026#34; end def install ENV[\u0026#34;LIBTOOL\u0026#34;] = \u0026#34;glibtool\u0026#34; args = %W[ --prefix=#{prefix} --cc=#{ENV.cc} --host-cc=#{ENV.cc} --disable-bsd-user --disable-guest-agent --enable-curses --enable-libssh --enable-vde --extra-cflags=-DNCURSES_WIDECHAR=1 --disable-cocoa --enable-sdl --disable-gtk ] # Sharing Samba directories in QEMU requires the samba.org smbd which is # incompatible with the macOS-provided version. This will lead to # silent runtime failures, so we set it to a Homebrew path in order to # obtain sensible runtime errors. This will also be compatible with # Samba installations from external taps. args \u0026lt;\u0026lt; \u0026#34;--smbd=#{HOMEBREW_PREFIX}/sbin/samba-dot-org-smbd\u0026#34; system \u0026#34;./configure\u0026#34;, *args system \u0026#34;make\u0026#34;, \u0026#34;V=1\u0026#34;, \u0026#34;install\u0026#34; end test do expected = build.stable? ? version.to_s : \u0026#34;QEMU Project\u0026#34; assert_match expected, shell_output(\u0026#34;#{bin}/qemu-system-i386 --version\u0026#34;) resource(\u0026#34;test-image\u0026#34;).stage testpath assert_match \u0026#34;file format: raw\u0026#34;, shell_output(\u0026#34;#{bin}/qemu-img info FLOPPY.img\u0026#34;) end end 重新编译安装之后，就不会出现死锁能正常工作了。\n{:height=\u0026ldquo;360px\u0026rdquo; width=\u0026ldquo;450px\u0026rdquo;}\n参考链接 qemu 论坛关于 Catalina 无法运行的 bug 报告{:target=\u0026quot;_blank\u0026quot;}. github 修复改 bug 的 commit{:target=\u0026quot;_blank\u0026quot;}. ","permalink":"https://0xpoe.dev/posts/2019-11-03-%E8%A7%A3%E5%86%B3catalina%E6%97%A0%E6%B3%95%E8%BF%90%E8%A1%8Cqemu%E9%97%AE%E9%A2%98/","summary":"\u003cp\u003e最近在学习 Rust，并且在尝试使用 Rust 来做内核，跟着 \u003ca href=\"https://os.phil-opp.com/\"\u003eWriting an OS in Rust\u003c/a\u003e{:target=\u0026quot;_blank\u0026quot;} 系列博客学习，这个博客质量非常高，深入浅出。\u003c/p\u003e\n\u003cp\u003e但是最近遇到一个qemu的问题,当我将系统升级到最近的 macOS 10.15 Catalina 之后，qemu 无法运行，启动之后卡死。如下：\n\u003cimg loading=\"lazy\" src=\"/images/post-images/2019-11-03/qemu-crash.jpeg\"\u003e{:height=\u0026ldquo;360px\u0026rdquo; width=\u0026ldquo;450px\u0026rdquo;}\u003c/p\u003e\n\u003ch3 id=\"解决方案\"\u003e解决方案\u003c/h3\u003e\n\u003cp\u003e实际上卡死的原因是 Catalina 操作系统底层 API 变化导致的死锁。该死锁会导致 qemu 的 UI Cocoa 无法正常工作。\u003c/p\u003e\n\u003cp\u003e所以我们目前运行 qemu 的非 GUI 模式是正常的。\u003c/p\u003e\n\u003cp\u003e后来我在 qemu 的论坛发现有人报告这个 bug，并且有人已经修复了这个问题，但是还没有 release。但是也有人给出了其他解决方案，可以通过自己编译切换 UI 来解决这个问题。\u003c/p\u003e\n\u003ch5 id=\"步骤如下我使用homebrew安装其他安装方式也一样重新编译就行\"\u003e步骤如下（我使用homebrew安装，其他安装方式也一样，重新编译就行）：\u003c/h5\u003e\n\u003col\u003e\n\u003cli\u003e\u003ccode\u003ebrew edit qemu\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e添加依赖：sdl2，\u003ccode\u003edepends_on \u0026quot;sdl2\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e关闭 cocoa，将\u003ccode\u003e--enable-cocoa\u003c/code\u003e 改为 \u003ccode\u003e--disable-cocoa\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e打开 sdl，将\u003ccode\u003e--disable-sdl\u003c/code\u003e 改为 \u003ccode\u003e--enable-sdl\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ebrew reinstall --build-from-source qemu\u003c/code\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e最终的 Formula 文件：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-ruby\" data-lang=\"ruby\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eQemu\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"no\"\u003eFormula\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003edesc\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;x86 and PowerPC Emulator\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003ehomepage\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;https://www.qemu.org/\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003eurl\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;https://download.qemu.org/qemu-4.1.0.tar.xz\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003esha256\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;656e60218689bdeec69903087fd7582d5d3e72238d02f4481d8dc6d79fd909c6\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003ehead\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;https://git.qemu.org/git/qemu.git\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003ebottle\u003c/span\u003e \u003cspan class=\"k\"\u003edo\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003esha256\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;b8e20707ff317f4182fbd180f033f4a9f163e40fec54a442d9b227e2f39846db\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"ss\"\u003e:catalina\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003esha256\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;3b35079b1729b9b1bd58087794ccd9d40848b69d22a601c12b451791709298d6\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"ss\"\u003e:mojave\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003esha256\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;0ef458cc2c387f8bbb2bdbd0e4d6e2a21574eb735c1621ad8e0cd3e094288298\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"ss\"\u003e:high_sierra\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003esha256\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;68d05c70584e722c8109682ddc041a6271e0c36d102c7d3f55553ba3b4f39480\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"ss\"\u003e:sierra\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003eend\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003edepends_on\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;libtool\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"ss\"\u003e:build\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003edepends_on\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;pkg-config\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"ss\"\u003e:build\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003edepends_on\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;glib\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003edepends_on\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;gnutls\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003edepends_on\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;jpeg\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003edepends_on\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;libpng\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003edepends_on\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;libssh\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003edepends_on\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;libusb\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003edepends_on\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;lzo\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003edepends_on\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;ncurses\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003edepends_on\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;pixman\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003edepends_on\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;vde\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003edepends_on\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;sdl2\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c1\"\u003e# 820KB floppy disk image file of FreeDOS 1.2, used to test QEMU\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003eresource\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;test-image\u0026#34;\u003c/span\u003e \u003cspan class=\"k\"\u003edo\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eurl\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;https://dl.bintray.com/homebrew/mirror/FD12FLOPPY.zip\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003esha256\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;81237c7b42dc0ffc8b32a2f5734e3480a3f9a470c50c14a9c4576a2561a35807\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003eend\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003einstall\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"no\"\u003eENV\u003c/span\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;LIBTOOL\u0026#34;\u003c/span\u003e\u003cspan class=\"o\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;glibtool\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eargs\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"sx\"\u003e%W[\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sx\"\u003e      --prefix=\u003c/span\u003e\u003cspan class=\"si\"\u003e#{\u003c/span\u003e\u003cspan class=\"n\"\u003eprefix\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"sx\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sx\"\u003e      --cc=\u003c/span\u003e\u003cspan class=\"si\"\u003e#{\u003c/span\u003e\u003cspan class=\"no\"\u003eENV\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ecc\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"sx\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sx\"\u003e      --host-cc=\u003c/span\u003e\u003cspan class=\"si\"\u003e#{\u003c/span\u003e\u003cspan class=\"no\"\u003eENV\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ecc\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"sx\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sx\"\u003e      --disable-bsd-user\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sx\"\u003e      --disable-guest-agent\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sx\"\u003e      --enable-curses\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sx\"\u003e      --enable-libssh\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sx\"\u003e      --enable-vde\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sx\"\u003e      --extra-cflags=-DNCURSES_WIDECHAR=1\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sx\"\u003e      --disable-cocoa\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sx\"\u003e      --enable-sdl\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sx\"\u003e      --disable-gtk\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sx\"\u003e    ]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e# Sharing Samba directories in QEMU requires the samba.org smbd which is\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e# incompatible with the macOS-provided version. This will lead to\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e# silent runtime failures, so we set it to a Homebrew path in order to\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e# obtain sensible runtime errors. This will also be compatible with\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e# Samba installations from external taps.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eargs\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u0026lt;\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;--smbd=\u003c/span\u003e\u003cspan class=\"si\"\u003e#{\u003c/span\u003e\u003cspan class=\"no\"\u003eHOMEBREW_PREFIX\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e/sbin/samba-dot-org-smbd\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nb\"\u003esystem\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;./configure\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003eargs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nb\"\u003esystem\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;make\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;V=1\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;install\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003eend\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nb\"\u003etest\u003c/span\u003e \u003cspan class=\"k\"\u003edo\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eexpected\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ebuild\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003estable?\u003c/span\u003e \u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eversion\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eto_s\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;QEMU Project\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eassert_match\u003c/span\u003e \u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eshell_output\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"si\"\u003e#{\u003c/span\u003e\u003cspan class=\"n\"\u003ebin\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e/qemu-system-i386 --version\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eresource\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;test-image\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003estage\u003c/span\u003e \u003cspan class=\"n\"\u003etestpath\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eassert_match\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;file format: raw\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eshell_output\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"si\"\u003e#{\u003c/span\u003e\u003cspan class=\"n\"\u003ebin\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e/qemu-img info FLOPPY.img\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003eend\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eend\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e重新编译安装之后，就不会出现死锁能正常工作了。\u003c/p\u003e","title":"解决 macOS 10.15 Catalina 无法运行 qmeu 的问题"},{"content":"👋 My name is Dongpo Liu and I’m a passionate software engineer who specializes in distributed systems and Rust.\nAs a software engineer at PingCAP, my main focus is on enhancing TiDB, which is an open-source distributed SQL database. I am also a core maintainer of TiCDC, which is a Change Data Capture (CDC) system for TiDB.\nI am also a big believer in open source. I contribute to the Rust community, where I am work on Cargo.\nProgramming languages I primarily code in: Rust/Go/TypeScript/JavaScript.\nLanguages I speak:\nEnglish (Professional working proficiency) Chinese (Native proficiency) ","permalink":"https://0xpoe.dev/about/","summary":"\u003cp\u003e👋 My name is Dongpo Liu and I’m a passionate software engineer who specializes in distributed systems and Rust.\u003c/p\u003e\n\u003cp\u003eAs a software engineer at \u003ca href=\"https://www.pingcap.com/\"\u003ePingCAP\u003c/a\u003e, my main focus is on enhancing \u003ca href=\"https://github.com/pingcap/tidb\"\u003eTiDB\u003c/a\u003e, which is an open-source distributed SQL database. I am also a core maintainer of \u003ca href=\"https://github.com/pingcap/tiflow\"\u003eTiCDC\u003c/a\u003e, which is a Change Data Capture (CDC) system for \u003ca href=\"https://github.com/pingcap/tidb\"\u003eTiDB\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eI am also a big believer in open source. I contribute to the Rust community, where I am work on \u003ca href=\"https://github.com/rust-lang/cargo\"\u003eCargo\u003c/a\u003e.\u003c/p\u003e","title":"About Me"}]