diff --git a/.github/workflows/mkbook.yml b/.github/workflows/mkbook.yml index 2380dd7f0..aa35755a3 100644 --- a/.github/workflows/mkbook.yml +++ b/.github/workflows/mkbook.yml @@ -28,30 +28,21 @@ jobs: # Build job build: runs-on: ubuntu-latest - env: - MDBOOK_VERSION: 0.4.36 steps: - uses: actions/checkout@v4 - - name: Prepare - run: | - mkdir mdbook - mv docs mdbook/src - - name: Install mdBook + - name: Install Depdencies run: | - curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf -y | sh - rustup update - cargo install --version ${MDBOOK_VERSION} mdbook + python3 -m pip install -r ./docs/requirements.txt - name: Setup Pages id: pages - uses: actions/configure-pages@v4 - - name: Build with mdBook - run: mdbook build - working-directory: mdbook + run: | + cd docs + mkdocs build - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: - path: ./mdbook/book + path: ./docs/site # Deployment job deploy: @@ -63,4 +54,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml new file mode 100644 index 000000000..28d101e76 --- /dev/null +++ b/.github/workflows/translate.yml @@ -0,0 +1,29 @@ +name: 'translator' +on: + issues: + types: [opened, edited] + issue_comment: + types: [created, edited] + discussion: + types: [created, edited] + discussion_comment: + types: [created, edited] + pull_request_target: + types: [opened, edited] + pull_request_review_comment: + types: [created, edited] + +jobs: + translate: + permissions: + issues: write + discussions: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: lizheming/github-translate-action@1.1.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + IS_MODIFY_TITLE: true + APPEND_TRANSLATION: false diff --git a/README.md b/README.md index 2c854ee05..16f067ef6 100644 --- a/README.md +++ b/README.md @@ -40,29 +40,12 @@ English | [中文版](README_CN.md) # Key features -- **Transparent Scalability** - - An OceanBase cluster can be scaled to 1,500 nodes transparently, handling petabytes of data and a trillion rows of records. - -- **Ultra-fast Performance** - - The only distributed database that has refreshed both the TPC-C record, at 707 million tmpC, and the TPC-H record, at 15.26 million QphH @30000GB. - -- **Real-time Operational Analytics** - - A unified system for both transactional and real-time operational analytics workloads. - -- **Continuous Availability** - - OceanBase Database adopts the Paxos Consensus algorithm to achieve Zero RPO and less than 8 seconds of RTO. Supports intra-city/remote disaster recovery, enabling multi-activity in multiple locations and zero data loss. - -- **MySQL Compatible** - - OceanBase Database is highly compatible with MySQL, which ensures that zero or a few modifications are needed for migration. - -- **Cost Efficiency** - - The cutting-edge compression technology saves 70%–90% of storage costs without compromising performance. The multi-tenancy architecture achieves higher resource utilization. +- **Transparent Scalability**: 1,500 nodes, PB data and a trillion rows of records in one cluster. +- **Ultra-fast Performance**: TPC-C 707 million tmpC and TPC-H 15.26 million QphH @30000GB. +- **Cost Efficiency**: saves 70%–90% of storage costs. +- **Real-time Analytics**: supports HTAP without additional cost. +- **Continuous Availability**: RPO = 0(zero data loss) and RTO < 8s(recovery time) +- **MySQL Compatible**: easily migrated from MySQL database. See also [key features](https://en.oceanbase.com/product/opensource) for more details. @@ -87,17 +70,25 @@ obd demo ## 🐳 Start with docker +**Note**: We provide images on [dockerhub](https://hub.docker.com/r/oceanbase/oceanbase-ce/tags), [quay.io](https://quay.io/repository/oceanbase/oceanbase-ce?tab=tags) and [ghcr.io](https://github.com/oceanbase/docker-images/pkgs/container/oceanbase-ce). If you have problems pulling images from dockerhub, please try the other two registries. + 1. Start an OceanBase Database instance: ```shell # Deploy a mini standalone instance. docker run -p 2881:2881 --name oceanbase-ce -e MODE=mini -d oceanbase/oceanbase-ce + + # Deploy a mini standalone instance using image from quay.io. + # docker run -p 2881:2881 --name oceanbase-ce -e MODE=mini -d quay.io/oceanbase/oceanbase-ce + + # Deploy a mini standalone instance using image from ghcr.io. + # docker run -p 2881:2881 --name oceanbase-ce -e MODE=mini -d ghcr.io/oceanbase/oceanbase-ce ``` 2. Connect to the OceanBase Database instance: ```shell - docker exec -it oceanbase-ce ob-mysql sys # Connect to the root user of the sys tenant. + docker exec -it oceanbase-ce obclient -h127.0.0.1 -P2881 -uroot # Connect to the root user of the sys tenant. ``` See also [Docker Readme](https://github.com/oceanbase/docker-images/tree/main/oceanbase-ce) for more details. @@ -107,7 +98,7 @@ See also [Docker Readme](https://github.com/oceanbase/docker-images/tree/main/oc You can deploy and manage OceanBase Database instance in kubernetes cluster with [ob-operator](https://github.com/oceanbase/ob-operator) quickly. Refer to the document [Quick Start for ob-operator](https://oceanbase.github.io/ob-operator) to see details. ## 👨‍💻 Start developing -See [OceanBase Developer Document](https://oceanbase.github.io/oceanbase/build-and-run.html) to learn how to compile and deploy a manually compiled observer. +See [OceanBase Developer Document](https://oceanbase.github.io/oceanbase/build-and-run) to learn how to compile and deploy a manually compiled observer. # Roadmap diff --git a/README_CN.md b/README_CN.md index c21dbdd7f..34b5aa65a 100644 --- a/README_CN.md +++ b/README_CN.md @@ -39,29 +39,13 @@ # 关键特性 -- **水平扩展** +- **水平扩展**:单机群支持超过1500节点、PB级数据量和单表超万亿行数据; +- **极致性能**:TPC-C 7.07亿tmpC和TPC-H 1526 万 QphH @30000GB; +- **低成本**:存储成本节省70%-90%; +- **实时分析**:不需要额外开销,支持HTAP; +- **高可用**:RPO = 0(0数据丢失),RTO < 8秒(恢复时间); +- **MySQL 兼容**:很容易的从MySQL迁移过来。 - 实现透明水平扩展,支持业务快速的扩容缩容,同时通过准内存处理架构实现高性能。支持集群节点超过数千个,单集群最大数据量超过 3PB,最大单表行数达万亿级。 - -- **极致性能** - - 唯一一个刷新了 TPC-C 记录(7.07 亿 tmpC)和 TPC-H 记录(1526 万 QphH @30000GB)的分布式数据库。 - -- **实时分析** - - 基于“同一份数据,同一个引擎”,同时支持在线实时交易及实时分析两种场景,“一份数据”的多个副本可以存储成多种形态,用于不同工作负载,从根本上保持数据一致性。 - -- **高可用** - - 独创“三地五中心”容灾架构方案,建立金融行业无损容灾新标准。支持同城/异地容灾,可实现多地多活,满足金融行业 6 级容灾标准(RPO=0,RTO< 8s),数据零丢失。 - -- **MySQL 兼容** - - 高度兼容 MySQL,覆盖绝大多数常见功能,支持过程语言、触发器等高级特性,提供自动迁移工具,支持迁移评估和反向同步以保障数据迁移安全,可支撑金融、政府、运营商等关键行业核心场景。 - -- **低成本** - - 基于 LSM-Tree 的高压缩引擎,存储成本降低 70% - 90%;原生支持多租户架构,同集群可为多个独立业务提供服务,租户间数据隔离,降低部署和运维成本。 更多信息请参考 [OceanBase 产品](https://www.oceanbase.com/product/oceanbase)。 @@ -86,28 +70,36 @@ obd demo ## 🐳 使用 docker +**注意**: 我们在 [dockerhub](https://hub.docker.com/r/oceanbase/oceanbase-ce/tags), [quay.io](https://quay.io/repository/oceanbase/oceanbase-ce?tab=tags) 和 [ghcr.io](https://github.com/oceanbase/docker-images/pkgs/container/oceanbase-ce) 提供镜像。如果您在从 dockerhub 拉取镜像时遇到问题,请尝试其他两个镜像库。 + 1. 启动 OceanBase 数据库实例 ```shell # 部署一个mini模式实例 docker run -p 2881:2881 --name oceanbase-ce -e MODE=mini -d oceanbase/oceanbase-ce + + # 使用 quay.io 仓库的镜像部署 OceanBase. + # docker run -p 2881:2881 --name oceanbase-ce -e MODE=mini -d quay.io/oceanbase/oceanbase-ce + + # 使用 ghcr.io 仓库的镜像部署 OceanBase. + # docker run -p 2881:2881 --name oceanbase-ce -e MODE=mini -d ghcr.io/oceanbase/oceanbase-ce ``` 2. 连接 OceanBase ```shell - docker exec -it oceanbase-ce ob-mysql sys # 连接root用户sys租户 + docker exec -it oceanbase-ce obclient -h127.0.0.1 -P2881 -uroot # 连接root用户sys租户 ``` 更多信息参考[docker 文档](https://github.com/oceanbase/docker-images/tree/main/oceanbase-ce)。 ## ☸️ 使用 Kubernetes -使用 [ob-operator](https://github.com/oceanbase/ob-operator) 可在 Kubernetes 环境中快速部署和管理 OceanBase 数据库实例,可参考文档 [ob-operator 快速上手](https://oceanbase.github.io/ob-operator/README-CN.html)了解具体的使用方法。 +使用 [ob-operator](https://github.com/oceanbase/ob-operator) 可在 Kubernetes 环境中快速部署和管理 OceanBase 数据库实例,可参考文档 [ob-operator 快速上手](https://oceanbase.github.io/ob-operator/zh-Hans/)了解具体的使用方法。 ## 👨‍💻 使用源码编译部署 -参考 [OceanBase 开发者文档](https://oceanbase.github.io/oceanbase/build-and-run.html)了解如何编译和部署手动编译的observer。 +参考 [OceanBase 开发者文档](https://oceanbase.github.io/oceanbase/build-and-run)了解如何编译和部署手动编译的observer。 # Roadmap diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md deleted file mode 100644 index e6cb97a96..000000000 --- a/docs/SUMMARY.md +++ /dev/null @@ -1,14 +0,0 @@ -# Summary - -[OceanBase Development Guide](README.md) -[Install toolchain](toolchain.md) -[Get the code, build and run](build-and-run.md) -[Set up an IDE](ide-settings.md) -[Coding Convensions](coding-convension.md) -[Write and run unit tests](unittest.md) -[Running MySQL test](mysqltest.md) -[Debug](debug.md) -[Logging System](logging.md) -[Memory Management](memory.md) -[Containers](container.md) -[Coding Standard](coding_standard.md) diff --git a/docs/docs/assets/favicon.ico b/docs/docs/assets/favicon.ico new file mode 100644 index 000000000..9f938fad4 Binary files /dev/null and b/docs/docs/assets/favicon.ico differ diff --git a/docs/docs/assets/logo.png b/docs/docs/assets/logo.png new file mode 100644 index 000000000..9c3714d78 Binary files /dev/null and b/docs/docs/assets/logo.png differ diff --git a/docs/README.md b/docs/docs/en/README.md similarity index 88% rename from docs/README.md rename to docs/docs/en/README.md index eaa59a521..f21781dde 100644 --- a/docs/README.md +++ b/docs/docs/en/README.md @@ -17,13 +17,16 @@ At present, the guide is composed of the following parts: 5. [Write and run unit tests](unittest.md) 6. [Running MySQL test](mysqltest.md) 7. [Debug](debug.md) - 8. Commit code and submit a pull request + 8. [Commit code and submit a pull request](contributing.md) More information before you start a big feature developing, you should read content below and it can help you understand oceanbase better. + 1. [Logging System](logging.md) 2. [Memory Management](memory.md) - 3. [Containers](container.md) - 4. [Coding Standard](coding_standard.md) + 3. [Bais Data Structures](container.md) + 4. [Architecture](architecture.md) + 5. [Coding Standard](coding_standard.md) + 3. **Contribute to OceanBase**: helps you quickly get involved in the OceanBase community, which illustrates what contributions you can make and how to quickly make one. ## User documents diff --git a/docs/docs/en/architecture.md b/docs/docs/en/architecture.md new file mode 100644 index 000000000..5aca64b93 --- /dev/null +++ b/docs/docs/en/architecture.md @@ -0,0 +1,39 @@ +--- +title: Architecture +--- + +# OceanBase DataBase Architecture + +OceanBase Database adopts a shared-nothing architecture, where each node is equal to each other. That is, each node has its own SQL engine, storage engine, and transaction engine, and runs on a cluster of ordinary PC servers. OceanBase Database provides core features such as high scalability, high availability, high performance, cost-effectiveness, and strong compatibility with mainstream databases. + +## Zone + +A cluster of OceanBase database consists of several nodes. These nodes belong to several availability zones, and each node belongs to an availability zone. Availability zone is a logical concept. OceanBase uses zones to achieve high availability and disaster recovery features of data. Availability zones can be in different computer rooms, regions, etc., to achieve disaster recovery in different scenarios. OceanBase uses the strong consistency protocol Paxos to achieve high availability. Data under the same Paxos group is located in different availability zones. + +## Partition + +In the OceanBase database, the data of a table can be horizontally split into multiple shards according to certain partitioning rules. Each shard is called a table partition, or Partition. Partition support includes hash, range, list and other types, and also supports secondary partitioning. For example, the order table in the transaction database can first be divided into several first-level partitions according to user ID, and then each first-level partition can be divided into several second-level partitions according to the month. For a second-level partition table, each sub-partition of the second level is a physical partition, while the first-level partition is just a logical concept. Several partitions of a table can be distributed on multiple nodes within an Availability Zone. Each physical partition has a storage layer object used to store data, called Tablet, which is used to store ordered data records. + +## Log Stream + +When the user modifies the records in the Tablet, in order to ensure data persistence, the redo log (REDO) needs to be recorded to the Log Stream corresponding to the Tablet. One log stream corresponds to multiple Tablets on the node where it is located. Tablet uses a multi-replication mechanism to ensure high availability. Typically, replicas are spread across different Availability Zones. Among multiple replicas, only one replica accepts modification operations, which is called the leader, and the others are called followers. Data consistency between leader and follower replicas is achieved through a distributed consensus protocol based on Multi-Paxos. Multi-Paxos uses Log Stream to implement data replication. Tablets can be migrated between Log Streams to achieve load balancing of resources. + +## OBServer + +A service process called observer runs on each node in the cluster. Each service is responsible for accessing partitioned data on its own node, and is also responsible for parsing and executing SQL statements routed to the node. These service processes communicate with each other through the TCP/IP protocol. At the same time, each service will listen for connection requests from external applications, establish connections and database sessions, and provide database services. + +## Multi-Tenant + +In order to simplify the management of large-scale deployment of multiple business databases and reduce resource costs, OceanBase database provides multi-tenant features. Within an OceanBase cluster, multiple isolated database "instances" can be created, called a tenant. From an application perspective, each tenant is equivalent to an independent database instance. Each tenant can choose MySQL or Oracle compatibility mode. After the application is connected to the MySQL tenant, users and databases can be created under the tenant, and the experience is the same as using an independent MySQL instance. After a new cluster is initialized, there will be a special tenant named sys, called the system tenant. The system tenant stores the metadata of the cluster and it is in MySQL compatibility mode. + +In addition to the system tenant and user tenant, OceanBase also has a tenant called Meta. Every time a user tenant is created, the system will automatically create a corresponding Meta tenant, whose life cycle is consistent with the user tenant. The Meta tenant is used to store and manage the cluster private data of user tenants. This data does not require cross-instance physical synchronization and physical backup and recovery. The data includes: configuration items, location information, replica information, log stream status, backup and recovery related information, merge information, etc. + +## Resource Unit + +In order to isolate tenant resources, each observer process can have multiple virtual containers belonging to different tenants, called resource units. Resource units include CPU, memory and disk resources. Multiple resource units form a resource pool. Using the resource pool, you can specify which resource unit to use, how many resource units, and availability zones for resource distribution. When creating a tenant, specify the list of resource pools used to control the resources and data distribution locations used by the tenant. + +## obproxy + +Applications usually do not directly establish a connection with OBServer, but connect to obproxy, and then obproxy forwards the SQL request to the appropriate OBServer node. obproxy will cache information related to data partitions and can route SQL requests to the most appropriate OBServer node. Obproxy is a stateless service. Multiple obproxy nodes can provide a unified network address to applications through network load balancing (SLB). + + diff --git a/docs/build-and-run.md b/docs/docs/en/build-and-run.md similarity index 100% rename from docs/build-and-run.md rename to docs/docs/en/build-and-run.md diff --git a/docs/coding-convension.md b/docs/docs/en/coding-convension.md similarity index 99% rename from docs/coding-convension.md rename to docs/docs/en/coding-convension.md index 8e7fa5a4c..54f288ecb 100644 --- a/docs/coding-convension.md +++ b/docs/docs/en/coding-convension.md @@ -1,3 +1,7 @@ +--- +title: Coding Convention +--- + OceanBase is a giant project that has been developed for more than ten years and contains millions of lines of C++ code. It already has many unique programming habits. Here are some OceanBase programming habits to help people who come into contact with the OceanBase source code for the first time have an easier time accepting and understanding. For more detailed information, please refer to ["OceanBase C++ Coding Standard"](./coding_standard.md). # Naming Convention diff --git a/docs/coding_standard.md b/docs/docs/en/coding_standard.md similarity index 99% rename from docs/coding_standard.md rename to docs/docs/en/coding_standard.md index 6310311fa..eecc2c7b3 100644 --- a/docs/coding_standard.md +++ b/docs/docs/en/coding_standard.md @@ -1,3 +1,6 @@ +--- +title: Coding Standard +--- | Number | Document Version | Revised Chapter | Reason for Revision | Revision Date | | -------| ---------------- | --------------- | ------------------- | ------------- | diff --git a/docs/container.md b/docs/docs/en/container.md similarity index 99% rename from docs/container.md rename to docs/docs/en/container.md index 7ddda4b12..5e1cf25f6 100644 --- a/docs/container.md +++ b/docs/docs/en/container.md @@ -1,3 +1,7 @@ +--- +title: Basic Data Structures +--- + # Introduction C++ STL provides many convenient containers, such as vector, map, unordered_map, etc. Due to OceanBase programming style and memory control, the use of STL containers is prohibited in OceanBase. OceanBase provides some container implementations, including arrays, linked lists, HashMap, etc. This document will introduce some of these containers. diff --git a/docs/docs/en/contributing.md b/docs/docs/en/contributing.md new file mode 100644 index 000000000..109b725bd --- /dev/null +++ b/docs/docs/en/contributing.md @@ -0,0 +1,85 @@ +# OceanBase Contributing Guidelines + +Thank you for considering contributing to OceanBase. + +We welcome any type of contribution to the OceanBase community. You can contribute code, help our new users in the DingTalk group (Group No.: 33254054), [Slack group](https://join.slack.com/t/oceanbase/shared_invite/zt-1e25oz3ol-lJ6YNqPHaKwY_mhhioyEuw) or in [StackOverflow](https://stackoverflow.com/search?q=oceanbase&s=4b3eddc8-73a2-4d90-8039-95fcc24e6450), test releases, or improve our documentation. + +If you are interested in contributing code to OceanBase, please read the following guidelines and follow the steps. + +## Start off with the right issue + +As a new contributor, you can start off by looking through our [good first issues](https://github.com/oceanbase/oceanbase/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). If none of them feels right for you, you can create a new issue when you find one. If you find an issue for you, please assign this issue to yourself in the issue topic and add the _developing_ label to indicate that this issue is being developed. + +## Contribute code changes + +1. Fork the OceanBase repository. + + 1. Visit the [OceanBase GitHub repository](https://github.com/oceanbase/oceanbase). + 2. Click the **Fork** button in the upper-right corner to fork one branch. + +2. Configure the local environment. + +```bash +working_dir=$HOME/{your_workspace} # Define the working directory of your choice. +user={github_account} # Make sure the user name is the same as your GitHub account. +``` + +3. Git clone the code. + +```bash +mkdir -p $working_dir +cd $working_dir +git clone git@github.com:$user/oceanbase.git + +# Add the upstream branch. +cd $working_dir/oceanbase +git remote add upstream git@github.com:oceanbase/oceanbase.git +# Or choose: git remote add upstream https://github.com/oceanbase/oceanbase + +# Set no_push for the upstream branch. +git remote set-url --push upstream no_push + +# Check if the upstream works as expected. +git remote -v +``` + +4. Create a new branch. + +```bash +# Check out the local master. +new_branch_name={issue_id} # Define the branch name. It is recommended that you use {issue+id} for the branch name. +cd $working_dir/oceanbase +git fetch upstream +git checkout master +git rebase upstream/master +git checkout -b $new_branch_name +``` + +5. Finish all the developing tasks in the new branch, including the testing tasks. +6. Submit changes to your branch. + +``` +# Check the local status. +git status + +# Add files for submission. +# Directly add all changes using `git add .` +git add ... +# In order to relate the pull request automatically to the issue, +# it is recommended that the commit message contain "fixed #{issue_id}". +git commit -m "fixed #{issue_id}: {your_commit_message}" + +# Sync upstream before push. +git fetch upstream +git rebase upstream/master +git push -u origin $new_branch_name +``` + +7. Create a pull request. + 1. Visit your fork repository. + 2. Click the **Compare & pull request** button next to the {new_branch_name} to create a pull request. +8. Sign the [Contributor License Agreement (CLA)](https://cla-assistant.io/oceanbase/oceanbase) aggreement. The workflow can continue only after you sign the CLA aggreement. + +![CLA](https://user-images.githubusercontent.com/5435903/204097095-6a19d2d1-ee0c-4fb6-be2d-77f7577d75d2.png#crop=0&crop=0&crop=1&crop=1&from=url&id=Mmj8a&margin=%5Bobject%20Object%5D&originHeight=271&originWidth=919&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=) + +After you submit your updates, OceanBase will review and comment if needed. Once we approve your updates, the system will automatically run CI testing and stress testing. If no issues are found in the tests, your updates will be merged. Now you have successfully completed your contribution task and become one of our contributors. diff --git a/docs/debug.md b/docs/docs/en/debug.md similarity index 99% rename from docs/debug.md rename to docs/docs/en/debug.md index 1424c618c..337d35720 100644 --- a/docs/debug.md +++ b/docs/docs/en/debug.md @@ -1,3 +1,7 @@ +--- +title: Debug +--- + # Abstract This document describes some methods to debug OceanBase. We have many ways to debug OceanBase, such as gdb, logging, etc. diff --git a/docs/ide-settings.md b/docs/docs/en/ide-settings.md similarity index 99% rename from docs/ide-settings.md rename to docs/docs/en/ide-settings.md index e4e00a368..adcc12105 100644 --- a/docs/ide-settings.md +++ b/docs/docs/en/ide-settings.md @@ -1,3 +1,7 @@ +--- +title: Set up an IDE +--- + # Abstract In order to easily read the code of OceanBase, we suggest using one IDE which is easily index the symbols of OceanBase. In Windows, we recommend `Souce Insight` can be used, and in Mac or Linux, we recommend that `VSCode + ccls` can be used to read the oceanbase code. Due to it is very easy to use `Source Ingisht`, so this document skip introduction how to use `Souce Insight`. diff --git a/docs/images/download-debug-info-package.png b/docs/docs/en/images/download-debug-info-package.png similarity index 100% rename from docs/images/download-debug-info-package.png rename to docs/docs/en/images/download-debug-info-package.png diff --git a/docs/images/ide-settings-ccls-index-example.png b/docs/docs/en/images/ide-settings-ccls-index-example.png similarity index 100% rename from docs/images/ide-settings-ccls-index-example.png rename to docs/docs/en/images/ide-settings-ccls-index-example.png diff --git a/docs/images/ide-settings-ccls-indexing.png b/docs/docs/en/images/ide-settings-ccls-indexing.png similarity index 100% rename from docs/images/ide-settings-ccls-indexing.png rename to docs/docs/en/images/ide-settings-ccls-indexing.png diff --git a/docs/images/ide-settings-ccls-keyboard-settings.png b/docs/docs/en/images/ide-settings-ccls-keyboard-settings.png similarity index 100% rename from docs/images/ide-settings-ccls-keyboard-settings.png rename to docs/docs/en/images/ide-settings-ccls-keyboard-settings.png diff --git a/docs/images/ide-settings-ccls-keyboard-settings2.png b/docs/docs/en/images/ide-settings-ccls-keyboard-settings2.png similarity index 100% rename from docs/images/ide-settings-ccls-keyboard-settings2.png rename to docs/docs/en/images/ide-settings-ccls-keyboard-settings2.png diff --git a/docs/images/ide-settings-ccls-plugin-settings.png b/docs/docs/en/images/ide-settings-ccls-plugin-settings.png similarity index 100% rename from docs/images/ide-settings-ccls-plugin-settings.png rename to docs/docs/en/images/ide-settings-ccls-plugin-settings.png diff --git a/docs/images/ide-settings-ccls-plugin.png b/docs/docs/en/images/ide-settings-ccls-plugin.png similarity index 100% rename from docs/images/ide-settings-ccls-plugin.png rename to docs/docs/en/images/ide-settings-ccls-plugin.png diff --git a/docs/images/ide-settings-ccls-threads-config.png b/docs/docs/en/images/ide-settings-ccls-threads-config.png similarity index 100% rename from docs/images/ide-settings-ccls-threads-config.png rename to docs/docs/en/images/ide-settings-ccls-threads-config.png diff --git a/docs/images/ide-settings-choose-ssh-config.png b/docs/docs/en/images/ide-settings-choose-ssh-config.png similarity index 100% rename from docs/images/ide-settings-choose-ssh-config.png rename to docs/docs/en/images/ide-settings-choose-ssh-config.png diff --git a/docs/images/ide-settings-connect-to-remote-server.png b/docs/docs/en/images/ide-settings-connect-to-remote-server.png similarity index 100% rename from docs/images/ide-settings-connect-to-remote-server.png rename to docs/docs/en/images/ide-settings-connect-to-remote-server.png diff --git a/docs/images/ide-settings-cpp-plugins.png b/docs/docs/en/images/ide-settings-cpp-plugins.png similarity index 100% rename from docs/images/ide-settings-cpp-plugins.png rename to docs/docs/en/images/ide-settings-cpp-plugins.png diff --git a/docs/images/ide-settings-input-password.png b/docs/docs/en/images/ide-settings-input-password.png similarity index 100% rename from docs/images/ide-settings-input-password.png rename to docs/docs/en/images/ide-settings-input-password.png diff --git a/docs/images/ide-settings-remote-plugin-usage.png b/docs/docs/en/images/ide-settings-remote-plugin-usage.png similarity index 100% rename from docs/images/ide-settings-remote-plugin-usage.png rename to docs/docs/en/images/ide-settings-remote-plugin-usage.png diff --git a/docs/images/ide-settings-remote-plugin.png b/docs/docs/en/images/ide-settings-remote-plugin.png similarity index 100% rename from docs/images/ide-settings-remote-plugin.png rename to docs/docs/en/images/ide-settings-remote-plugin.png diff --git a/docs/images/ide-settings-use-different-ssh-port.png b/docs/docs/en/images/ide-settings-use-different-ssh-port.png similarity index 100% rename from docs/images/ide-settings-use-different-ssh-port.png rename to docs/docs/en/images/ide-settings-use-different-ssh-port.png diff --git a/docs/images/unittest-ci-details.png b/docs/docs/en/images/unittest-ci-details.png similarity index 100% rename from docs/images/unittest-ci-details.png rename to docs/docs/en/images/unittest-ci-details.png diff --git a/docs/images/unittest-github-ci.png b/docs/docs/en/images/unittest-github-ci.png similarity index 100% rename from docs/images/unittest-github-ci.png rename to docs/docs/en/images/unittest-github-ci.png diff --git a/docs/images/unittest-unittest.png b/docs/docs/en/images/unittest-unittest.png similarity index 100% rename from docs/images/unittest-unittest.png rename to docs/docs/en/images/unittest-unittest.png diff --git a/docs/logging.md b/docs/docs/en/logging.md similarity index 99% rename from docs/logging.md rename to docs/docs/en/logging.md index 07fb090e9..9a6db9148 100644 --- a/docs/logging.md +++ b/docs/docs/en/logging.md @@ -1,3 +1,7 @@ +--- +title: System Log +--- + # OceanBase System Log Introduction ## Introduction diff --git a/docs/memory.md b/docs/docs/en/memory.md similarity index 99% rename from docs/memory.md rename to docs/docs/en/memory.md index 3c4c0f16c..2d62675ed 100644 --- a/docs/memory.md +++ b/docs/docs/en/memory.md @@ -1,3 +1,7 @@ +--- +title: Memory Management +--- + # Introduction Memory management is one of the most important modules in any large C++ project. Since OceanBase also needs to deal with the issue of multi-tenant memory resource isolation, OceanBase's memory management is more complicated than ordinary C++ projects. Generally, a good memory management module needs to consider the following issues: diff --git a/docs/mysqltest.md b/docs/docs/en/mysqltest.md similarity index 100% rename from docs/mysqltest.md rename to docs/docs/en/mysqltest.md diff --git a/docs/toolchain.md b/docs/docs/en/toolchain.md similarity index 100% rename from docs/toolchain.md rename to docs/docs/en/toolchain.md diff --git a/docs/unittest.md b/docs/docs/en/unittest.md similarity index 100% rename from docs/unittest.md rename to docs/docs/en/unittest.md diff --git a/docs/docs/zh/README.md b/docs/docs/zh/README.md new file mode 100644 index 000000000..b557aa9b0 --- /dev/null +++ b/docs/docs/zh/README.md @@ -0,0 +1,35 @@ +# OceanBase 开发者手册 + +## 介绍 + +* **面向人群** 手册的目标受众是OceanBase的贡献者,无论是新人还是老手。 +* **目标** 手册的目标是帮助贡献者成为OceanBase的专家,熟悉其设计和实现,从而能够在现实世界中流畅地使用它以及深入开发OceanBase本身。 + +## 手册结构 + +当前,手册由以下部分组成: + +1. **开始**: 设置开发环境,构建和连接到OceanBase服务器,子部分基于一个想象的新手用户旅程。 + 1. [安装工具链](toolchain.md) + 2. [获取代码,编译运行](build-and-run.md) + 3. [配置IDE](ide-settings.md) + 4. [编程惯例](coding-convension.md) + 5. [编写并运行单元测试](unittest.md) + 6. [运行MySQL测试](mysqltest.md) + 7. [调试](debug.md) + 8. [提交代码和Pull Request](contributing.md) + +2. **OceanBase设计和实现**: 介绍了OceanBase的设计和实现细节,这些细节对于理解OceanBase的工作原理至关重要。 + 在开始编写稍大的功能之前,你应该阅读以下内容,它可以帮助你更好地理解OceanBase。 + + 1. [日志系统](logging.md) + 2. [内存管理](memory.md) + 3. [基础数据结构](container.md) + 4. [架构](architecture.md) + 5. [编程规范](coding_standard.md) + +## 用户文档 + +本手册不包含用户文档。 + +可以参考 [oceanbase-doc](https://github.com/oceanbase/oceanbase-doc) 查看用户文档。 diff --git a/docs/docs/zh/architecture.md b/docs/docs/zh/architecture.md new file mode 100644 index 000000000..cc3958c57 --- /dev/null +++ b/docs/docs/zh/architecture.md @@ -0,0 +1,39 @@ +--- +title: 架构 +--- + +# OceanBase 数据库架构 + +OceanBase采用了无共享(Shared-Nothing)的集群架构,集群由若干完全对等的计算机(称为节点)组成,每个节点都有其私有的物理资源(包括CPU、内存、硬盘等),并且在这些节点上运行着独立的存储引擎、SQL引擎、事务引擎等。集群中各个节点之间相互独立,但通过连接彼此的网络设备相互协调,共同作为一个整体完成用户的各种请求。由于节点之间的独立性,使得 OceanBase 具备可扩展、高可用、高性能、低成本等核心特性。 + +![architecture](images/architecture.jpg) + +## 可用区(Zone) + +OceanBase 数据库的一个集群由若干个节点组成。这些节点分属于若干个可用区(Zone),每个节点属于一个可用区。可用区是一个逻辑概念,OceanBase使用zone来实现数据的高可用性和灾备特性。可用区可以在不同的机房、区域等,进而实现不同场景的容灾。OceanBase 使用强一致性协议 Paxos 实现高可用,同一个Paxos组下的数据,位于不同的可用区。 + +## 分区(Partition) + +在 OceanBase 数据库中,一个表的数据可以按照某种划分规则水平拆分为多个分片,每个分片叫做一个表分区,简称分区(Partition)。分区支持包括hash、range、list等类型,还支持二级分区。例如,交易库中的订单表,可以先按照用户 ID 划分为若干一级分区,再按照月份把每个一级分区划分为若干二级分区。对于二级分区表,第二级的每个子分区是一个物理分区,而第一级分区只是逻辑概念。一个表的若干个分区可以分布在一个可用区内的多个节点上。每个物理分区有一个用于存储数据的存储层对象,叫做 Tablet,用于存储有序的数据记录。 + +## 日志流(Log Stream) + +当用户对 Tablet 中记录进行修改的时候,为了保证数据持久化,需要记录重做日志(REDO)到 Tablet 对应的日志流(Log Stream)里。一个日志流对应其所在节点上的多个 Tablet。Tablet 使用多副本机制来保证高可用。一般来说,副本分散在不同的可用区里。多个副本中有且只有一个副本接受修改操作,叫做主副本(Leader),其它的叫做从副本(Follower)。主从副本之间通过基于 Multi-Paxos 的分布式共识协议实现了副本之间数据的一致性。而 Multi-Paxos 使用 Log Stream 来实现数据复制。Tablet 可以在Log Stream之间迁移,以实现资源的负载均衡。 + +## OBServer + +在集群的每个节点上会运行一个叫做 observer 的服务进程。每个服务负责自己所在节点上分区数据的存取,也负责路由到本机的 SQL 语句的解析和执行。这些服务进程之间通过 TCP/IP 协议进行通信。同时,每个服务会监听来自外部应用的连接请求,建立连接和数据库会话,并提供数据库服务。 + +## 多租户 + +为了简化大规模部署多个业务数据库的管理并降低资源成本,OceanBase 数据库提供了独特的多租户特性。在一个 OceanBase 集群内,可以创建多个相互隔离的数据库"实例",叫做一个租户。从应用程序的视角来看,每个租户等同于一个独立的数据库实例。每个租户可以选择 MySQL 或 Oracle 兼容模式。应用连接到 MySQL 租户后,可以在租户下创建用户、database,与一个独立的 MySQL 库的使用体验是一样的。一个新的集群初始化之后,会存在一个特殊的名为 sys 的租户,叫做系统租户。系统租户中保存了集群的元数据,是一个 MySQL 兼容模式的租户。 + +除了系统租户和用户租户,OceanBase 还有一个称为Meta的租户。每创建一个用户租户系统就会自动创建一个对应的 Meta 租户,其生命周期与用户租户保持一致。Meta 租户用于存储和管理用户租户的集群私有数据,这部分数据不需要进行跨库物理同步以及物理备份恢复,这些数据包括:配置项、位置信息、副本信息、日志流状态、备份恢复相关信息、合并信息等。 + +## 资源单元 + +为了隔离租户的资源,每个 observer 进程内可以有多个属于不同租户的虚拟容器,叫做资源单元(resource unit),资源单元包括 CPU 、内存和磁盘资源。多个资源单元组成一个资源池(resource pool),使用资源池可以指定使用哪个资源单元、多少个资源单元以及资源分布的可用区。创建租户时,指定所使用的资源池列表,这样控制租户使用的资源和数据分布位置。 + +## obproxy + +应用程序通常并不直接与 OBServer 建立连接,而是连接obproxy,然后由 obproxy 转发 SQL 请求到合适的 OBServer 节点。obproxy 会缓存数据分区相关的信息,可以将SQL请求路由到尽量合适的 OBServer 节点。obproxy 是无状态的服务,多个 obproxy 节点可以通过网络负载均衡(SLB)对应用提供统一的网络地址。 diff --git a/docs/docs/zh/build-and-run.md b/docs/docs/zh/build-and-run.md new file mode 100644 index 000000000..788d1505c --- /dev/null +++ b/docs/docs/zh/build-and-run.md @@ -0,0 +1,62 @@ +# 获取代码,编译运行 + +## 前置条件 + +检查支持的操作系统列表([安装工具链](toolchain.md))和GLIBC版本要求,以及如何安装C++工具链。 + +## Clone 代码 + +把代码clone到本地: + +```shell +git clone https://github.com/oceanbase/oceanbase.git +``` + +## 构建 + +构建debug或release版本的OceanBase源码: + +### Debug 模式 + +```shell +bash build.sh debug --init --make +``` + +### Release 模式 + +```shell +bash build.sh release --init --make +``` + +## 运行 + +`observer` 二进制文件已经编译出来了,可以使用 `obd.sh` 工具部署一个 OceanBase 实例: + +```shell +./tools/deploy/obd.sh prepare -p /tmp/obtest +./tools/deploy/obd.sh deploy -c ./tools/deploy/single.yaml +``` + +OceanBase 服务程序会监听 10000 端口。 + +## 连接 + +可以使用官方的 MySQL 客户端连接 OceanBase: + +```shell +mysql -uroot -h127.0.0.1 -P10000 +``` + +也可以使用 `obclient` 连接 OceanBase: + +```shell +./deps/3rd/u01/obclient/bin/obclient -h127.0.0.1 -P10000 -uroot -Doceanbase -A +``` + +## 停止 + +停止服务并清理部署: + +```shell +./tools/deploy/obd.sh destroy --rm -n single +``` diff --git a/docs/docs/zh/coding-convension.md b/docs/docs/zh/coding-convension.md new file mode 100644 index 000000000..29d9939ad --- /dev/null +++ b/docs/docs/zh/coding-convension.md @@ -0,0 +1,155 @@ +--- +title: 编程惯例 +--- + +OceanBase 编程惯例 + +OceanBase 是一个发展了十几年的、包含几百万行C++代码的巨型工程,它已经有了很多自己特有的编程习惯,这里介绍一些首次接触OceanBase源码同学一些需要注意的事项,也可以让大家更方便的阅读OceanBase源码。更详细的内容可以参考[OceanBase C++编程规范](coding_standard.md)。 + +# 命名习惯 +## 文件命名 +OceanBase中代码文件名都以`ob_`开头。但也有一些陈旧的例外文件。 + +## 类命名 +类都以`Ob`开头,也有一些陈旧类的意外。 + +## 成员变量命名 +成员变量都以`_`作为后缀。 + +# 函数编程习惯 +## 禁止使用STL容器 +由于OceanBase支持多租户资源隔离,为了方便控制内存,OceanBase禁止使用STL、boost等容器。同时,OceanBase提供了自己实现的容器,比如 `ObSEArray` 等。 + +## 单入口单出口 +强制要求所有函数在末尾返回,禁止中途调用return、goto、exit等全局跳转指令。这一条也是所有首次接触OceanBase代码的人最迷惑的地方。 +为了实现这一要求,代码中会出现很多 `if/else if` ,并且在 `for` 循环中存在 `OB_SUCC(ret)` 等多个不那么直观的条件判断。同时为了减少嵌套,会使用宏 `FALSE_IT` 执行某些语句。比如 + +```cpp +int ObMPStmtReset::process() +{ + int ret = OB_SUCCESS; + ... + if (OB_ISNULL(req_)) { + ret = OB_INVALID_ARGUMENT; + LOG_WARN("invalid packet", K(ret), KP(req_)); + } else if (OB_INVALID_STMT_ID == stmt_id_) { + ret = OB_INVALID_ARGUMENT; + LOG_WARN("stmt_id is invalid", K(ret)); + } else if (OB_FAIL(get_session(session))) { + LOG_WARN("get session failed"); + } else if (OB_ISNULL(session)) { + ret = OB_ERR_UNEXPECTED; + LOG_WARN("session is NULL or invalid", K(ret), K(session)); + } else if (OB_FAIL(process_kill_client_session(*session))) { + LOG_WARN("client session has been killed", K(ret)); + } else if (FALSE_IT(session->set_txn_free_route(pkt.txn_free_route()))) { + } else if (OB_FAIL(process_extra_info(*session, pkt, need_response_error))) { + LOG_WARN("fail get process extra info", K(ret)); + } else if (FALSE_IT(session->post_sync_session_info())) { + } else if (FALSE_IT(need_disconnect = false)) { + } else if (OB_FAIL(update_transmission_checksum_flag(*session))) { + LOG_WARN("update transmisson checksum flag failed", K(ret)); + } else { + // ... + } + return ret; +} +``` + +代码中使用了大量的 `if/else if` ,并且使用了FALSE_IF宏尽量减少if的嵌套。 + +值得一提的是,类似的函数中都会在函数开头写 `int ret = OB_SUCCESS;`,将ret作为函数返回值,并且很多宏,也会默认ret的存在。 + +## 函数返回错误码 +对于绝大多数函数,都要求函数具备int返回值,返回值可以使用错误码 `ob_errno.h` 解释。 +这里说的绝大多数函数,包含一些获取值的函数,比如 ObSEArray的at函数 +```cpp +int at(int64_t idx, T &obj); +``` + +哪些函数不需要返回int值? +比较简单的返回类属性的函数,比如 ObSEArray 的函数: +```cpp +int64_t get_capacity(); +``` +将直接返回值,而不带int错误码。 +或者类似的简单判断的函数不需要返回int错误码。 + +## 需要判断所有函数值与函数参数有效性 +OceanBase 要求只要函数有返回值,就必须对返回值做检测,做到“能检就检”。对于函数参数,特别是指针,在使用前都必须检查其有效性。 +比如: +```cpp +int ObDDLServerClient::abort_redef_table(const obrpc::ObAbortRedefTableArg &arg, sql::ObSQLSessionInfo *session) +{ + int ret = OB_SUCCESS; + ... + obrpc::ObCommonRpcProxy *common_rpc_proxy = GCTX.rs_rpc_proxy_; + if (OB_UNLIKELY(!arg.is_valid())) { // 这里对传入的参数做有效性检查 + ret = OB_INVALID_ARGUMENT; + LOG_WARN("invalid arg", K(ret), K(arg)); + } else if (OB_ISNULL(common_rpc_proxy)) { // 在使用指针之前,也要做检查 + ret = OB_ERR_UNEXPECTED; + LOG_WARN("common rpc proxy is null", K(ret)); + } else { + ... + } + return ret; +} +``` + +# 一些约定函数接口 +## init/destroy +OceanBase 要求在构造函数中,仅实现一些非常轻量级的数据初始化工作,比如变量初始化为0,指针初始化为nullptr等。因为构造函数中,不太容易处理一些复杂的异常场景,并且无法给出返回值。OceanBase绝大多数的类都有 init 函数,通常在构造函数之后执行,并且拥有int错误码作为返回值。这里做一些比较复杂的初始化工作。相对应的,通常也会提供destroy函数做资源销毁工作。 + +## reuse/reset +内存缓存是提高性能非常有效的手段。OceanBase 很多类都会有reuse/reset接口,以方便某个对象后续重用。reuse 通常表示轻量级的清理工作,而reset会做更多的资源清理工作。但是需要看具体实现类,不能一概而论。 +## 操作符重载 +C++ 提供了非常方便编写程序的运算符重载功能,但是这些重载往往会带来很多负担,使得代码难以阅读、功能误用。比如运算符重载可能会导致程序员不知情的情况下出现类型隐式转换、或者看起来一个简单的操作却有比较高的开销。 +另外,要尽量避免使用 `operator=` ,尽量以`deep_copy`、`shallow_copy`的方式实现对象的复制。 + +# 常用宏 +OB_SUCC + +判断某个语句是否返回成功,等价于OB_SUCCESS == (ret = func()) +```cpp +ret = OB_SUCCESS; +if (OB_SUCC(func())) { + // do something +} +``` +OB_FAIL + +类似OB_SUCC,只是判断某个语句是否执行失败: +```cpp +ret = OB_SUCCESS; +if (OB_FAIL(func())) { + // do something +} +``` +OB_ISNULL + +判断指针是否为空,等价于nullptr == ptr, +```cpp +if (OB_ISNULL(ptr)) { + // do something +} +``` +OB_NOT_NULL + +判断指针是否非空,等价于nullptr != ptr, +```cpp +if (OB_NOT_NULL(ptr)) { + // do something +} +``` + +K + +通常用在输出日志中,用法 K(obj),其中obj可以是普通类型变量,也可以是类对象(必须实现to_string),会被展开会 `"obj", obj`,最终在日志中会输出 "obj=123"。比如: +```cpp +LOG_WARN("fail to exec func, ", K(ret)); +``` + +DISALLOW_COPY_AND_ASSIGN + +用在类的声明中,表示此类禁止复制赋值等操作。 diff --git a/docs/docs/zh/coding_standard.md b/docs/docs/zh/coding_standard.md new file mode 100644 index 000000000..5280c9799 --- /dev/null +++ b/docs/docs/zh/coding_standard.md @@ -0,0 +1,2758 @@ +--- +title: 编程规范 +--- + +| 编号 | 文档版本 | 修订章节 | 修订原因 | 修订日期 | +| --- | ------ | -------- | ------ | ------- | +| 1 | 0.1 | 全文 | 初版 | 2023/5/23 | + +# 1 引言 +本编码规范适用于蚂蚁金服OceanBase项目,给出了一些编码约束,并定义了编码风格。OceanBase项目中,测试代码必须遵守本文档的编码风格,建议测试代码也同时遵守本文档的编码约束;其他代码必须遵守本文档的编码约束和编码风格。 + +本编码规范致力于书写出通俗易懂、减少陷阱、格式统一的C/C++代码,因此: + +- 采用最常见、最易懂的方式编写代码; +- 避免采用任何冷僻方式,例如:foo(int x = 1); +- 避免非常技巧的方式,例如:a+=b; b=a-b; a -= b; 或者:a^=b; b^=a; a^=b;以交换变量a和b的值; + +本文最后对编码约束进行了小结,以便快速查阅。 +本编码规范将根据需要不断进行补充和完善。 + +# 2 目录和文件 +## 2.1 目录结构 +OceanBase系统的子目录说明如下: +- src:存放源代码,包含头文件和实现文件 +- unittest:存放单元测试代码和开发人员编写的小规模集成测试代码 +- tests:存放测试团队的测试框架和用例 +- tools:存放外部工具 +- doc:存放文档 +- rpm:存放rpm spec文件 +- script:存放OceanBase的运维脚本 + +C代码的实现文件命名为.c,头文件命名为.h,C++代码的实现文件命名为.cpp,头文件命名为.h。原则上头文件和实现文件必须一一对应,src下的目录和unittest下的目录一一对应。所有文件命名统一使用全部英文小写字母,单词之间使用'_'分割。 +例如,src下的common目录有一个头文件ob_schema.h和实现文件ob_schema.cpp,相应地,在unittest目录下也有一个common目录,其中有一个名字叫做test_schema.cpp的单元测试文件。 + +当然,开发人员也会做一些模块内或者多个模块的集成测试,这些测试代码也放到unittest,但是所在的子目录和文件名不要求和src一一对应。例如,基线存储引擎的集成测试代码放到unittest/storagetest目录中。 + +## 2.2 版权信息 +目前(2021-3),observer & obproxy所有源代码文件头中必须使用如下版权信息: +```cpp +Copyright (c) 2021 OceanBase +OceanBase is licensed under Mulan PubL v2. +You can use this software according to the terms and conditions of the Mulan PubL v2. +You may obtain a copy of Mulan PubL v2 at: + http://license.coscl.org.cn/MulanPubL-2.0 +THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, +EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, +MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +See the Mulan PubL v2 for more details. +``` + +## 2.3 头文件代码 +头文件不能出现实现代码,内联函数或者C++模板除外。另外,如果模板代码太长,可以将其中部分或者全部模板函数提取到专门的.ipp文件中,具体可参考common/ob_vector.h和common/ob_vector.ipp。 + +头文件应尽可能简洁清晰,方便使用者理解。如果某些函数的实现代码较短,希望直接放到头文件中,要求将函数声明为内联函数,并且符合内联函数的规范。 + +## 2.4 define保护 +所有头文件都应该使用#define防止头文件被多重包含,命名格式为:`___`。例如:common模块中的头文件`common/ob_schema.h`按如下方式保护: + +```cpp +#ifndef OCEANBASE_COMMON_OB_SCHEMA_ +#define OCEANBASE_COMMON_OB_SCHEMA_ +... +#endif // OCEANBASE_COMMON_OB_SCHEMA_ +``` + +## 2.5 头文件依赖 +在头文件中使用其他类,尽量采用前置声明的方式,避免使用 `#include`。 + +当一个头文件被包含的同时也引入了新的依赖,一旦该头文件被修改,代码就会被重新编译。如果这个头文件又包含了其他头文件,这些头文件的任何改变都将导致所有包含了该头文件的代码被重新编译。因此,我们倾向于减少包含头文件,尤其是在头文件中包含头文件。 + +使用前置声明可以显著减少需要包含的头文件数量。举例说明:如果头文件中用到类 ObFoo但不需要访问 ObFoo类的声明,头文件中只需前置声明 `class ObFoo;` 而无须 `#include "ob_foo.h"`。 + +不允许访问类的定义的前提下,我们在一个头文件中能对类 ObFoo 做哪些操作? +- 我们可以将数据成员类型声明为Ob Foo * 或Ob Foo &. +- 我们可以将函数参数 / 返回值的类型声明为 ObFoo (但不能定义实现). +- 我们可以将静态数据成员的类型声明为 ObFoo, 因为静态数据成员的定义在类定义之外. + +反之,如果你的类是ObFoo 的子类,或者含有类型为 ObFoo 的非静态数据成员,则必须包含`ob_foo.h`头文件。 + +当然,如果使用指针成员代替对象成员会降低代码可读性或者执行效率,那么,不要仅仅为了避免#include而这么做。 + +## 2.6 内联函数(inline) +为了提高执行效率,有时候我们需要使用内联函数,但需要对内联函数的运行机制有所了解。建议只有在性能关键点,执行代码少于10行,不包含循环或者switch语句,未使用递归机制才使用内联函数。 + +内联函数在C++类中,应用最广泛的就是用来定义存取函数。一方面,内联该函数可以避免函数调用开销,使得目标代码更加高效;另一方面,每一处内联函数的调用都要复制代码,将使得程序的总代码量膨胀。如果函数体内的代码比较长,或者函数体内出现循环,执行函数体内代码的时间要比函数调用的开销大,则不宜使用内联。 + +类的构造函数和析构函数容易引起误解。它们可能看起来很短,不过要当心可能隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造和析构函数。 + +## 2.7 #include的路径和顺序 +项目内头文件应该按照项目目录树结构引入,不要使用特殊路径,类似'.','..'等。建议包含头文件的顺序为本文件对应的头文件,系统c头文件,系统c++头文件,其它库头文件(libeasy,tbsys),OceanBase内部其它头文件,避免出现多重包含。其中,系统c头文件用尖括号,末尾加.h,系统C++头文件用尖括号,末尾不加.h,其它情况用引号,例如 + +```cpp +#include , +#include , +#include "common/ob_schema.h" +``` + +之所以将本文件对应的头文件放到优先位置,是为了减少隐藏依赖。我们希望每一个头文件独立编译,最简单的实现方式是将其作为第一个.h文件包含在对应的.cpp文件中。 +例如ob_schema.cpp的include包含次序如下: + +```cpp +#include "common/ob_schema.h" + +#include +#include +#include + +#include + +#include "config.h" +#include "tblog.h" + +#include "common/utility.h" +#include "common/ob_obj_type.h" +#include "common/ob_schema_helper.h" +#include "common/ob_define.h" +#include "common/file_directory_utils.h" +``` + +## 2.8 总结 +1.src和unittest中的子目录一一对应,tests用来放测试代码。 +2.头文件不能包含实现代码,内联函数和模板除外。 +3.通过define保护避免头文件被多重包含。 +4.通过前置声明降低编译依赖,防止修改一个文件引发多米诺效应。 +5.内联函数的合理使用可以提高执行代码效率。 +6.项目内文件的include路径为相对路径,include顺序为:本文件对应的头文件,系统c头文件,系统c++头文件,其它库头文件(Libeasy,tbsys),OceanBase 内部其它头文件。 + +# 3 作用域 +## 3.1 命名空间 +OceanBase源代码中的所有变量、函数以及类都通过命名空间区分开来,命名空间和代码所处的目录一一对应。例如,`src/common`目录下的`ob_schema.h`对应的命名空间为 `oceanbase::common`。 + +```cpp +// .h文件 +namespace oceanbase +{ +// 注意不要缩进 +namespace common +{ +// 所有声明都置于命名空间中,注意不要缩进 +class ObSchemaManager +{ +public: + int func(); +}; +} // namespace common +} // namespace oceanbase +``` + +```cpp +// .cpp文件 +namespace oceanbase +{ +namespace common +{ +// 所有函数实现都置于命名空间中 +int ObSchemaManager::func() +{ + … +} + +} // namespace common +} // namespace oceanbase +``` + +禁止使用匿名命名空间,这是因为,编译器会给匿名命名空间分配一个随机的命名字符串,这将会影响GDB调试。 +头文件和实现文件中都可能包含对其它命名空间中类的引用。例如,在头文件中声明其它命名空间的类,如下: + +```cpp +namespace oceanbase +{ +namespace common +{ +class ObConfigManager; // 类common::ObConfigManager的前置声明 +} + +namespace chunkserver +{ + +class ObChunkServer +{ +public: + int func(); +}; + +} // namespace chunkserver +} // namespace oceanbase +``` + +C++允许使用 using,分为两种情况: +1.using指令(using directive):例如 `using namespace common`,以后编译器会自动在 `common` 命名空间中查找符号; +2.using声明(using declaration):例如 `using common::ObSchemaManager`,以后 `ObSchemaManager` 相当于 `common::ObSchemaManager`。 + +考虑到 `using` 指令很容易污染作用域,因此,禁止在.h文件中使用 `using` 指令,但允许在.h文件中使用 `using` 声明。 +.cpp文件中允许使用using指令,例如,`ObChunkServer` 实现时需要引用 `common` 名字空间的类。需要注意的是:.cpp文件中只能通过 `using` 指令引入其它命名空间,.cpp文件自身的代码还是需要放到所在的命名空间中,例如: + +```cpp +// 错误的写法 +// 实现类代码应该放到chunkserver名字空间中而不是using namespace chunkserver; +namespace oceanbase +{ +using namespace common; +using namespace chunkserver; + +// 使用common命名空间的符号 +int ObChunkServer::func() +{ + ... func函数实现... +} + +} // namespace oceanbase +``` + +```cpp +// 正确的写法,实现类代码放到了chunkserver命名空间中 +namespace oceanbase +{ +using namespace common; + +namespace chunkserver +{ +// 使用common命名空间的符号 +int ObChunkServer::func() +{ + ...func函数实现... +} + +} // namespace chunkserver +} // namespace oceanbase +``` + +## 3.2 嵌套类 +如果一个类是另外一个类的成员,可以定义为嵌套形式。嵌套类也称为成员类。 + +```cpp +class ObFoo +{ +private: + // ObBar是嵌套在ObFoo中的成员类/嵌套类,ObFoo称为宿主类/外部类 +class ObBar +{ + ... +}; +}; +``` + +当嵌套类只被外部类使用时,将其置于外部类作用域,从而避免污染其他作用域的同名类。另外,建议在外部类的.h文件中前置声明嵌套类,在.cpp文件中定义嵌套类的实现,这样可以避免外部类的.h文件中包含嵌套类的实现,提高可读性。 + +需要注意的是,尽量避免将嵌套类定义为public,除非它们是对外接口的一部分。 + +## 3.3 全局变量与全局函数 +严格限制全局变量或全局函数的使用,除了已有的全局变量和全局函数外,不得增加新的全局变量和全局函数。如果必须违反,请事先讨论经过同意,并详细注释原因。 + +全局变量和全局函数带来一系列问题,例如命名冲突,又如全局对象初始化顺序不确定。如果一定要全局共享某个变量,应该放到服务器的单例,例如 `UpdateServer` 的 `ObUpdateServerMain` 中。 + +全局常量统一定义在 `ob_define.h` 中,全局函数统一定义在 `common/ob_define.h` 和 `utility` 方法(`common/utility.h`,`common/ob_print_utils.h`)中。 + +**禁止头文件中定义全局const变量** + +和“禁止头文件中定义static变量”原因类似,没有显式 `extern` 的全局 `const` 变量(包括 `constexpr`)也具有internal linkage,也会在二进制程序中产生多份副本。 + +实验分析 + +```cpp +// "a.h" +const int zzz = 1000; +extern const int bbb; +// "a.cpp" +#include "a.h" +#include +const int bbb = 2000; +void func1() +{ + printf("a.cpp &zzz=%p\n", &zzz); + printf("a.cpp &bbb=%p\n", &bbb); +} +// "b.cpp" +#include "a.h" +#include +void func2() +{ + printf("b.cpp &zzz=%p\n", &zzz); + printf("b.cpp &bbb=%p\n", &bbb); +} +// "main.cpp" +void func2(); +void func1(); +int main(int argc, char *argv[]) +{ + func1(); + func2(); + return 0; +} +``` + +编译并执行,可以看到变量zzz产生了多个实例,变量bbb只有一个实例。 + +``` +[abc@OceanBase224004 tmp]$ ./a.out +a.cpp &zzz=0x4007e8 +a.cpp &bbb=0x400798 +b.cpp &zzz=0x400838 +b.cpp &bbb=0x400798 +``` + +## 3.4 局部变量 +在语句块开始处(由{}组成)声明变量,强制要求简单变量声明时就初始化。 + +OceanBase认为需要在每个语句块(由{}组成)的开始处声明,这样的代码的可读性较好。另外,允许 `for (int +i = 0; i < 10; ++i)` (i在for循环开始处声明,相当于循环语句块的开头)这样的代码。如果声明变量和使用变量的地方相隔较远,说明语句块包含的代码过多,这往往意味着需要进行代码重构。 + +在循环体内声明变量,如果变量是一个对象,每次循环都要先后调用其构造函数和析构函数,每次循环也需要圧栈和弹栈,因此将这样的变量提取到循环外要高效得多。禁止在循环体内声明非简单变量(例如类变量),如果必须违反,请事先征得小组负责人的同意,并详细注释原因。出于代码可读性的考虑,允许在循环内声明引用。 + +```cpp +// 低效的实现 +for (int i = 0; i < 100000; ++i) { + ObFoo f; // 每次进入循环都要调用构造函数和析构函数 + f.do_something(); +} + +// 高效的实现 +ObFoo f; +for (int i = 0; i < 100000; ++i) { + f.do_something(); +} + +//出于代码可读性的考虑,可以在循环内声明引用 +for(int i = 0; i < N; ++i) { + const T &t = very_long_variable_name.at(i); + t.f1(); + t.f2(); + ... +} +``` + +此外,OB对局部变量的大小进行限制,不建议定义过大的局部变量。 +1. 函数栈不超过32K。 +2. 单个局部变量不超过8K。 + +## 3.5 静态变量 + +**禁止头文件中定义static变量** + +除了以下一种例外,不允许在.h头文件中定义初始化static变量(无论是不是const)。否则,这样的static变量在每个编译单元(.o文件)中会产生一个静态区存储的变量,链接后会有多个静态变量实例。如果是const变量,造成编译后二进制程序文件膨胀。如果不是const变量,则可能造成严重的bug。 + +> 注意,是禁止定义(define),不是禁止声明(declare)。 + +【例外】static const/constexpr静态成员变量 + +类里的 `static const int`(包括int32_t, int64_t, uint32_t, uint64_t等),`static constexpr double` 等静态成员变量,我们常用来定义 hardcode 数组长度等,是不占存储的,没有地址(可以视为 `#define` 宏常量),允许在头文件中定义初始化。 + +也就是说,下面这种形式(伪代码)是允许的: + +```cpp +class Foo { + static const/constexpr xxx = yyy; +}; +``` + +对这条例外的解释如下。在C++98中,允许static const整型变量在声明时定义值。 + +```cpp +class ObBar +{ +public: + static const int CONST_V = 1; +}; +``` + +事实上,C++编译器认为上面这段代码等价于: + +```cpp +class ObBar +{ + enum { CONST_V = 1 }; +} +``` + +如果在程序中取这种变量的地址,链接的时候会产生"Undefined reference"错误。对于这种情况,正确的做法,还是要把定义放入.cpp中。 + +```cpp +// 在.h中 +class ObBar +{ + static const int CONST_V; +} + +// 在.cpp中: +const int ObBar::CONST_V = 1; +``` + +在C++11之前,C++98标准只允许 intergral 类型 `static const` 变量在类声明中包含定义进行初始化。在C++11中,引入了constexpr,用 `static constexpr` 成员变量(包括double等类型)也可以在声明中进行初始化。这种变量在编译后也不会产生静态区存储。 + +```cpp +constexpr double earth_gravitational_acceleration = 9.8; +constexpr double moon_gravitational_acceleration = earth_gravitational_acceleration / 6.0; +``` + +**案例一** + +按照现在OB的代码风格,我们会在头文件中定义static的变量(例如ob_define.h),这样每个cpp文件include这个头文件的时候都会生成一次这个变量的声明和定义。特别是有些大对象(latch,wait event等)在头文件生成了一份static的定义,导致生成binany和内存膨胀的比较厉害。 + +简单将几个static变量的定义由头文件移至cpp文件,头文件改成extern定义,效果还是比较明显: +binary大小: 2.6G->2.4G ,减少200M + +observer初始运行内存:6.3G->5.9G,减少400M + +**案例二** + +下面这个例子中,不同的 cpp 看到的是全局变量的不同副本。本来预期是通过全局 static 来通信,结果变成了各说各话。这样还会造成“假”的singleton实现。 + +**static变量行为分析** + +我们写一个小程序来验证一下static变量定义在.h中的表现。 + +```cpp +// "a.h" +static unsigned char xxx[256]= +{ + 1, 2, 3 +}; +static unsigned char yyy = 10; +static const unsigned char ccc = 100; +// "a.cpp" +#include "a.h" +#include +void func1() +{ + printf("a.cpp &xxx=%p\n", xxx); + printf("a.cpp &yyy=%p\n", &yyy); + printf("a.cpp &ccc=%p\n", &ccc); +} +// "b.cpp" +#include "a.h" +#include +void func2() +{ + printf("b.cpp xxx=%p\n", xxx); + printf("b.cpp &yyy=%p\n", &yyy); + printf("b.cpp &ccc=%p\n", &ccc); +} +// "main.cpp" +void func2(); +void func1(); +int main(int argc, char *argv[]) +{ + func1(); + func2(); + return 0; +} +``` + +编译并执行,可以看到无论是静态整数还是数组,无论有没有const,都产生了多个实例。 + +```cpp +[OceanBase224004 tmp]$ g++ a.cpp b.cpp main.cpp +[OceanBase224004 tmp]$ ./a.out +a.cpp &xxx=0x601060 +a.cpp &yyy=0x601160 +a.cpp &ccc=0x400775 +b.cpp xxx=0x601180 +b.cpp &yyy=0x601280 +b.cpp &ccc=0x4007a2 +``` + +## 3.6 资源回收与参数恢复 +资源管理遵守“谁申请谁释放”的原则,并在语句块结束时统一释放资源。如果必须违反,请事先征得小组负责人的同意,并详细注释原因。 +每个语句块的代码结构如下: + +1. 变量定义 +2. 资源申请 +3. 业务逻辑 +4. 资源释放 + +```cpp +// 错误的用法 +void *ptr = ob_malloc(sizeof(ObStruct), ObModIds::OB_COMMON_ARRAY); +if (NULL == ptr) { + // print error log +} else { + if (OB_SUCCESS != (ret = do_something1(ptr))) { + // print error log + ob_tc_free(ptr, ObModIds::OB_COMMON_ARRAY); + ptr = NULL; + } else if (OB_SUCCESS != (ret = do_something2(ptr))) { + // print error log + ob_free(ptr, ObModIds::OB_COMMON_ARRAY); + ptr = NULL; + } else { } +} +``` + +```cpp +// 正确的用法 +void *ptr = ob_malloc(100, ObModIds::OB_COMMON_ARRAY); +if (NULL == ptr) { + // print error log +} else { + if (OB_SUCCESS != (ret = do_something1(ptr))) { + // print error log + } else if (OB_SUCCESS != (ret = do_something2(ptr))) { + // print error log + } else { } +} +// 释放资源 +if (NULL != ptr) { + ob_free(ptr, ObModIds::OB_COMMON_ARRAY); + ptr = NULL; +} +``` + +上面的例子中,最外层的if分支只是判断资源申请失败的情况,由else分支处理业务逻辑。因此,也可以将资源释放的代码放在最外层else分支的末尾。 + +```cpp +// 另外一种正确的写法,要求if分支只是简单处理资源申请失败 +void *ptr = ob_malloc(100, ObModIds::OB_COMMON_ARRAY); +if (NULL == ptr) { + // print error log +} else { + if (OB_SUCCESS != (ret = do_something1(ptr))) { +// print error log + } else if (OB_SUCCESS != (ret = do_something2(ptr))) { +// print error log + } else { } +// 释放资源 + ob_free(ptr, ObModIds::OB_COMMON_ARRAY); + ptr = NULL; +} +``` + +因此,如果需要释放资源,在函数返回前或者最外层else分支的末尾,统一释放资源。 +某些情况下需要在语句块的开头保存输入参数,并在异常时恢复参数。和资源回收类似,只能在语句块的结尾恢复参数。最典型的例子是序列化函数,例如: + +```cpp +// 错误的写法 +int serialize(char *buf, const int64_t buf_len, int64_t &pos) +{ + int ret = OB_SUCCESS; + const int64_t ori_pos = pos; + + if (OB_SUCCESS != (ret = serialize_one(buf, buf_len, pos))) { + pos = ori_pos; + ... + } else if (OB_SUCCESS != (ret = serialize_two(buf, buf_len, pos))) { + pos = ori_pos; + ... + } else { + ... + } + return ret; +} +``` + +这种用法的问题在于很可能会在某个分支忘记恢复pos的值,正确的写法如下: + +```cpp +// 正确的写法 +int serialize(char *buf, const int64_t buf_len, int64_t &pos) +{ + int ret = OB_SUCCESS; + const int64_t ori_pos = pos; + + if (OB_SUCCESS != (ret = serialize_one(buf, buf_len, pos))) { + ... + } else if (OB_SUCCESS != (ret = serialize_two(buf, buf_len, pos))) { + ... + } else { + ... + } + + if (OB_SUCCESS != ret) { + pos = ori_pos; + } + return ret; +} +``` + +因此如果需要恢复输入参数,在函数返回前恢复。 + +## 3.7 总结 +1.命名空间和目录对应,禁止使用匿名命名空间,.h文件中禁止使用using指令,只允许使用using声明。 +2.嵌套类适合用在只被外部类使用的场景,建议在.h文件中前置申明,在.cpp文件中实现,尽量不要用public。 +3.除了已有的全局变量和全局函数外,不得增加新的全局变量和全局函数。如果必须违反,请事先征得小组负责人的同意,并详细注释原因。 +4.局部变量在语句块开头处声明,强制要求简单变量声明时就初始化。 +5.禁止在循环体内声明非简单变量。如果必须违反,请事先征得小组负责人的同意,并详细注释原因。 +6.资源管理遵守“谁申请谁释放”的原则。如果需要释放资源,在函数返回前或者最外层else分支的末尾释放。因此如果需要恢复输入参数,在函数返回前恢复。如果必须违反,请事先征得小组负责人的同意,并详细注释原因。 + +# 4 类 +## 4.1 构造函数与析构函数 +构造函数只进行那些没有实际意义(trivial)的初始化工作,例如将指针初始化为NULL,将变量初始化为0或者-1。不允许在构造函数进行有实际意义(non-trivial)的初始化操作,如果需要,定义单独的 `int init()` 方法并增加一个 `is_inited_` 变量标识对象是否已经初始化成功。这是因为,如果对象构造失败,可能会出现不确定的状态。 + +每个类(包括接口类)都要求定义构造函数,即使该类没有任何成员变量,也需要定义一个空的默认构造函数。这是因为,如果没有定义任何构造函数,编译器会自动生成默认构造函数,这样往往会产生一些副作用,例如将某些成员变量初始化成随机值。 + +每个类(包括接口类)都要求定义析构函数,即使该类没有任何成员变量,也需要定义一个空的析构函数。另外,如果没有特殊原因(性能特别关键,不会被继承且不包含虚函数),都应该把类的析构函数声明为virtual。 + +## 4.2 explicit关键字 +**对单参数构造函数使用C++关键字 explicit。** + +通常,只有一个参数的构造函数可被用于转换,例如,定义了 `ObFoo::ObFoo(ObString name)`,当向需要传入一个 `ObFoo` 对象的函数传入一个 `ObString` 时,构造函数 `ObFoo::ObFoo(ObString name)` 会被自动调用并将该字符串转换为一个 `ObFoo` 临时对象传给调用函数。这种隐式转换总是带来一些潜在的bug。 + +## 4.3 拷贝构造函数 +原则上不得使用拷贝构造函数(已经定义使用的基础类除外)。如果必须违反,请事先征得小组负责人的同意,并详细注释原因。另外,不需要拷贝构造函数的类应使用 `DISALLOW_COPY_AND_ASSIGN` 宏(ob_define.h),接口类可以例外。 + +```cpp +#define DISALLOW_COPY_AND_ASSIGN(type_name) \ + type_name(const type_name&) \ + void operator=(const type_name&) + +class ObFoo +{ +public: + ObFoo(); + ~ObFoo(); + +private: + DISALLOW_COPY_AND_ASSIGN(ObFoo); +}; +``` + +## 4.4 reuse & reset & clear +reset用于重置对象,reuse用于重用对象,禁止使用clear。说明如下: + +1.reset和reuse都用于对象的重用。这种重用往往是因为某些关键类的对象分配内存和构造太耗时,为了优化性能而引入的。 +2.reset的含义是把对象的状态恢复成构造函数或者init函数执行以后的初始状态。参考ObRow::reset; +3.除了reset以外的其它情况采用reuse。与reset不同的是,reuse往往不释放某些重新申请很耗时的资源,如内存等。参考PageArena::reuse; +4.clear广泛用于C++ STL的容器类中,往往表示把容器类的size清0,但是并不清空内部的对象或者释放内存。clear和reset/reuse的区别很微妙,为了简化理解,禁止使用clear,已使用的逐步去除。 + +## 4.5 成员初始化 +必须初始化所有成员,且成员变量初始化顺序和定义顺序保持一致。 + +每个类的构造函数、init方法、reset/reuse方法都可能对类执行一些初始化操作,需要确保所有的成员都已经初始化。如果构造函数只初始化部分成员,那么,一定要将is_inited_变量设置为false,由init方法继续完成其它成员的初始化工作。struct类型的成员可以通过reset方法初始化(如果初始化仅仅是将struct成员清0,也可以使用memset初始化);class类型的成员可以通过init/reset/reuse等方法初始化。 + +成员变量的初始化顺序需要和定义顺序保持一致,这样的好处是很容易发现是否忘记初始化了哪些成员。 + +## 4.6 结构体和类 + +**仅当只有数据时使用struct,其他一概使用class。** + +struct 被用在仅包含数据的消极对象上,可能包括有关联的常量,以及 reset/is_valid,序列化/反序列化这几个通用函数。如果需要更多的函数功能,class 更加合适。如果不确定的话,直接使用 class。 +如果与STL结合,对于仿函数(functor)和萃取(traits)可以不用class而是使用struct。 +需要注意的是,class 内部的数据成员只能定义为私有的(private,静态成员可以例外),并通过存取函数 get_xxx 和 set_xxx 进行访问。 + +## 4.7 通用函数 +每个类包含的通用函数都必须采用标准原型,序列化/反序列化函数必须使用宏实现。 + +每个类包含的通用函数包括:`init`,`destroy`,`reuse`,`reset`,`deep_copy`,`shallow_copy`,`to_string`, `is_valid`。这些函数的原型如下: + +```cpp +class ObFoo +{ +public: + int init(init_param_list); + bool is_inited(); + void destroy(); + + void reset(); + void reuse(); + + int deep_copy(const ObFoo &src); + int shallow_copy(const ObFoo &src); + + bool is_valid(); + + int64_t to_string(char *buf, const int64_t buf_len) const; + + NEED_SERIALIZE_AND_DESERIALIZE; +}; +``` + +需要注意的是,to_string总是会在末尾补'\0',且函数返回实际打印的字节长度(不包括'\0')。内部通过调用databuff_printf相关函数实现,具体请参考common/ob_print_utils.h。 + +序列化和反序列化函数需要通过宏来实现,举个例子: + +```cpp +class ObSort +{ +public: + NEED_SERIALIZE_AND_DESERIALIZE; +private: + common::ObSArray sort_columns_; + int64_t mem_size_limit_; + int64_t sort_row_count_; +}; +``` + +对于类 `ObSort`,需要序列化的三个域是 +`sort_columns_`、 `mem_size_limit_` 和 `sort_row_count_`。在ob_sort.cpp里面只需要写上: + +```cpp +DEFINE_SERIALIZE_AND_DESERIALIZE(ObSort, sort_columns_, mem_size_limit_, sort_row_count_); +``` + +就能完成序列化和反序列化已经计算序列化以后得长度的三个函数的实现。 + +结构体的通用函数与类的通用函数相同。 + +## 4.8 常用宏 +为了方便编码,在OB中可以使用一些已经定义过的宏,但并不建议同学们自己新增宏,如果确实有新增宏的必要,请和小组负责人确认后再行添加。 +以下是一些常用的宏: + +1. OB_SUCC +通常用于判断返回值是否为OB_SUCCESS,等价于OB_SUCCESS == (ret = func()),注意使用OB_SUCC时需要在函数内前置定义ret,例如下面的写法, +```cpp +ret = OB_SUCCESS; +if (OB_SUCC(func())) { + // do something +} +``` + +2. OB_FAIL +通常用于判断返回值是否不为OB_SUCCESS,等价于 `OB_SUCCESS != (ret = func())`,注意使用 `OB_FAIL` 时需要在函数内前置定义ret,例如下面的写法, + +```cpp +ret = OB_SUCCESS; +if (OB_FAIL(func())) { + // do something +} +``` + +3. OB_ISNULL +通常用于判断指针是否为空,等价于 `nullptr ==`,例如下面的写法, + +```cpp +if (OB_ISNULL(ptr)) { + // do something +} +``` + +4. OB_NOT_NULL +通常用于判断指针是否不为空,等价于 `nullptr !=`,例如下面的写法, +```cpp +if (OB_NOT_NULL(ptr)) { + // do something +} +``` + +5. IS_INIT +通常用于判断类是否完成了初始化,等价于 `is_inited_`,注意在类中需要存在成员 `is_inited_`,例如下面的写法, + +```cpp +if (IS_INIT) { + // do something +} +``` + +6. IS_NOT_INIT +通常用于判断类是否完成了初始化,等价于 `!is_inited_`,注意在类中需要存在成员 `is_inited_`,例如下面的写法, + +```cpp +if (IS_NOT_INIT) { + // do something +} +``` + +7. REACH_TIME_INTERVAL +用于判断是否超过了某个时间间隔,参数为微秒,注意在宏内部会有一个静态变量记录时间,所以对时间的判断是全局的,通常用于控制日志输出频率,例如下面的写法,会让系统在超过1s间隔后,做一些动作。 + +```cpp +if (REACH_TIME_INTERVAL(1000 * 1000)) { + // do something +} +``` + +8. OZ +用于简化 `OB_FAIL` 之后的日志输出,当在报错后只需要简单输出日志时,可以使用 `OZ`,注意使用 `OZ` 时需要首先在cpp文件的开始定义 `USING_LOG_PREFIX`, 例如下面的写法, +```cpp +OZ(func()); +``` + + 等价于 +```cpp +if (OB_FAIL(func())) { + LOG_WARN("fail to exec func, ", K(ret)); +} +``` + +9. K +通常用于日志输出时,输出变量名与变量值,例如下面的写法, + +```cpp +if (OB_FAIL(ret)) { + LOG_WARN("fail to exec func, ", K(ret)); +} +``` + +10. KP +通常用于日志输出时,输出变量名与指针,例如下面的写法, + +```cpp +if (OB_FAIL(ret)) { + LOG_WARN("fail to exec func, ", K(ret), KP(ptr)); +} +``` + +## 4.9 继承 +所有继承必须是public的,且使用继承时必须谨慎:只有在“是一个”的情况下使用继承,在“有一个”的情况下使用组合。 + +当子类继承父类时,子类包含了父类的所有数据及操作定义。C++实践中,继承主要用于两种场景:实现继承,子类继承父类的实现代码;接口继承,子类继承父类的方法名称。对于实现继承,由于实现子类的代码在父类和子类间延展,要理解其实现变得更加困难,要谨慎使用。 + +OceanBase里面也用到了多重继承,这种场景是很少见的,并且要求最多只有一个基类中包含实现,其他基类都是纯接口类。 + +## 4.10 操作符重载 +除了容器类,自定义数据类型(`ObString`、`ObNumber` 等)以及 `ObRowkey`、`ObObj`、`ObRange` 等少量全局基础类以外,不要重载操作符(简单的结构的赋值操作除外)。如果必须违反,请事先讨论通过,并详细注释原因。 + +C++ STL模板类大量重载操作符,例如,比较函数,四则运算符,自增,自减,这样的代码貌似更加直观,其实往往混淆调用者,例如使得调用者误认为某些比较耗时的操作像内建操作一样高效。 + +除了简单的结构外,避免重载赋值操作(`operator=`)。如果需要的话,可以定义 `deep_copy`、`shallow_copy` 等拷贝函数。其中, `deep_copy` 表示所有成员都需要深拷贝,`shallow_copy` 表示其它情况。如果某些成员需要浅拷贝,某些需要深拷贝,那么,采用 `shallow_copy`。 +## 4.11 声明次序 +在头文件中使用特定的声明次序,public在private之前,成员函数在数据成员之前。 + +定义次序如下:public块,protected块,private块,每一块内部的次序如下: +1. typedefs和enums; +2. 常量; +3. 构造函数; +4. 析构函数; +5. 成员函数,静态成员函数在前,普通成员函数在后; +6. 数据成员,静态数据成员在前,普通数据成员在后; + +宏 `DISALLOW_COPY_AND_ASSIGN` 置于 `private:` 块之后,作为类的最后部分。 +.cpp文件中的函数定义应该尽可能和.h中的声明次序保持一致。 + +之所以要将常量定义放到函数定义(构造/析构函数,成员函数)的前面,而不是放到数据成员中,那是因为,常量可能被函数引用。 + +## 4.12 总结 +1. 构造函数只做trival的初始化工作,每个类都需要定义至少一个构造函数,带有虚函数或者子类的析构函数声明为virtual。 +2. 为了避免隐式类型转换,需要将单参数构造函数声明为explicit。 +3. 原则上不得使用拷贝构造函数(已经定义使用的基础类除外)。如果必须违反,请事先讨论通过,并详细注释原因。 +4. 使用 `DISALLOW_COPY_AND_ASSIGN` 避免拷贝构造函数、赋值操作滥用; +5. 类重置使用reset,重用使用reuse,禁止使用clear。 +6. 需要确保初始化所有成员,且成员变量初始化顺序和定义顺序保持一致。 +7. 仅在只有数据时使用struct,其他情况一概使用class。 +8. 每个类包含的通用函数都必须采用标准原型,序列化/反序列化函数必须使用宏实现。 +9. 优先考虑组合,只有在“是一个”关系时使用继承。避免私有继承和多重继承,多重继承使用时,要求除一个基类含有实现外,其他基类都是纯接口类。 +10. 除了已有的容器类、自定义类型以及少量全局基础类以外,不允许重载操作符(简单的结构的赋值操作除外)。如果必须违反,请事先征得小组负责人的同意,并详细注释原因。 +11. 声明次序:public->protected->private。 + +# 5 函数 +## 5.1 单入口单出口 +强制要求所有函数在末尾返回,禁止中途调用return、goto、exit等全局跳转指令。如果必须违反,请事先讨论,并详细注释原因。 + +OceanBase认为大规模项目开发应该优先避免常见陷阱,牺牲编程复杂度是值得的。而单入口单出口能够使得开发人员不容易忘记释放资源或者恢复函数输入参数。无论任何时候,都要求函数只有一个出口。 + +## 5.2 函数返回值 +除了如下几种例外,函数都必须返回ret错误码: +1. 简单存取函数`set_xxx()/get_xxx()`。如果`set/get`函数比较复杂或者可能出错,那么仍然必须返回错误码。 +2. 已经定义和使用的at(i)函数等(定义和使用新的,请事先征得小组负责人的同意,并详细注释原因)。 +3. 已经定义和使用的操作符重载(定义和使用新的,请事先征得小组负责人的同意,并详细注释原因)。 +4. 其他少量函数,例如类的通用函数 `void reset(); void reuse();` 等参见4.7节通用函数。 + +函数调用者必须检查函数的返回值(错误码)并处理。 + +只能用int类型的ret变量表示错误,且ret只能表示错误(迭代器函数由于历史原因除外)。如果需要返回其它类型的值,比如compare函数返回bool类型的值,需要采用其它变量名,例如bool_ret。例如: + +```cpp +// 错误的写法 +bool operator()(const RowRun &r1, const RowRun &r2) const +{ + bool ret = false; + + int err = do_something(); + + return ret; +} +``` + +```cpp +// 正确的写法 +bool operator()(const RowRun &r1, const RowRun &r2) const +{ + bool bool_ret = false; + + int ret = do_something(); + + return bool_ret; +} +``` + +如果函数执行过程中需要临时保存一些错误码,那么,尽量使用含义明确的变量,例如 `hash_ret`,`alloc_ret`。如果含义不明确,那么,也可以依次采用ret1,ret2,。。。来表示,避免使用err表示错误码引起混淆。例如: + +```cpp +int func() +{ + int ret = OB_SUCCESS; + + ret = do_something(); + if (OB_SUCCESS != ret) { + int alloc_ret = clear_some_resource (); + if (OB_SUCCESS != alloc_ret) { + // print error log + } + } else { + ... + } + return ret; +} +``` + +## 5.3 顺序语句 +如果多条顺序语句在做同一件事情,那么,在某些情况下可以采用精简写法。 + +由于函数执行过程中需要判断错误,这将使得顺序代码变得冗长。例如: + +```cpp +// 冗长的代码 +int ret = OB_SUCCESS; + +ret = do_something1(); +if (OB_SUCCESS != ret) { + // print error log +} + +if (OB_SUCCESS == ret) { + ret = do_something2(); + if (OB_SUCCESS != ret) { + // print error log + } +} + +// 更多代码... +``` + +可以看出,真正有效的代码只有两行,但是整体代码量是有效代码的好几倍。这将使得一屏包含的有效代码过少,影响可读性。 +如果顺序语句中每一步都只需要一行代码,那么,建议通过如下的方式精简代码: + +```cpp +// 当顺序逻辑中每一步只有一行代码时,使用精简写法 +int ret = OB_SUCCESS; + +if (OB_FAIL(do_something1())) { + // print error log +} else if (OB_FAIL(do_something2())) { + // print error log +} else if (OB_SUCCESS != (ret = do_something3())) { + // print error log +} else { } +``` + +如果顺序语句的某些步骤超过一行代码,那么,需要做一些变化: + +```cpp +// 当顺序逻辑中某些步骤超过一行代码,使用精简写法,并做一定变化 +int ret = OB_SUCCESS; + +if (OB_SUCCESS != (ret = do_something1())) { + // print error log +} else if (OB_SUCCESS != (ret = do_something2())) { + // print error log +} else { + // 步骤3执行的代码超过一行 + if (OB_SUCCESS != (ret = do_something3_1())) { + // print error log + } else if (OB_SUCCESS != (ret = do_something3_2())) { + // print error log +} else { } +} + +if (OB_SUCCESS == ret) { // 开始一段新的逻辑 + if (OB_SUCCESS != (ret = do_something4())) { +// print error log + } else if (OB_SUCCESS != (ret = do_something5())) { +// print error log + } else { } +} +``` + +实际编码过程中,什么时候应该采用精简写法呢?OceanBase认为,当顺序语句的每一步都只有一行语句,并且这些步骤逻辑上耦合比较紧,都应该尽量采用精简写法。然而,如果逻辑上属于多个部分,每个部分做不同的事情,那么,只应该在每个部分内部采用精简写法,而不是为了精简而精简。 + +需要注意的是,如果顺序语句后面紧接着条件语句。假如顺序语句采用精简写法变成条件语句,那么,不能将它们合并为一个大的条件语句,而应该将它们在代码结构上分开来。例如: + +```cpp +// 错误的写法 +if (OB_SUCCESS != (ret = do_something1())) { + // print error log +} else if (OB_SUCCESS != (ret = do_something2())) { + // print error log +} else if (cond) { + // do something if cond +} else { + // do something if !cond +} +``` + +```cpp +// 第一种正确的写法 +if (OB_SUCCESS != (ret = do_something1())) { + // print error log +} else if (OB_SUCCESS != (ret = do_something2())) { + // print error log +} else { + if (cond) { + // do something if cond + } else { + // do something if !cond + } +} +``` + +```cpp +// 第二种正确的写法 +if (OB_SUCCESS != (ret = do_something1())) { + // print error log +} else if (OB_SUCCESS != (ret = do_something2())) { + // print error log +} else { } + +if (OB_SUCCESS == ret) { + if (cond) { + // do something if cond + } else { + // do something if !cond + } +} +``` + +## 5.4 循环语句 +在循环条件中判断 `OB_SUCCESS == ret`,防止错误码被覆盖等问题。 + +OceanBase发现了大量错误码被覆盖的问题,这些问题往往都会导致严重后果,例如数据不一致,而且非常难以发现。例如: + +```cpp +// 错误码被覆盖 +for (int i = 0; i < 100; ++i) { + ret = do_something(); + if (OB_SUCCESS != ret) { + // print error log + } else { + ... + } +} +``` + +上面的例子中,for循环中的if分支判断错误,但是忘记break,这样,代码将会进入下一次循环,前一次执行的错误码将被覆盖。 +因此,规范for循环语句的写法如下: + +```cpp +for (int i = 0; OB_SUCCESS == ret && i < xxx; ++i) { + ... +} +``` + +另外,规范while循环语句的写法如下: + +```cpp +while (OB_SUCCESS == ret && other_cond) { + ... +} +``` + +循环语句中可能会用到break或者continue来改变执行路径。OceanBase认为应该尽量少用,这和函数单入口单出口的道理是一样的,相当于循环语句的后续代码的输入来源为多个入口,增加了代码的复杂度。如果确实有必要使用break和continue,要求通过注释详细说明原因,写代码或者Review代码时都需要格外关注。另外,考虑到后续代码的输入来源为多个入口,需要确保能说清楚后续代码的输入到底满足什么条件。 + +## 5.5 条件语句 +条件语句需要遵守MECE原则。 + +MECE一词来源于麦肯锡分析法,意思是相互独立,完全穷尽(Mutually +Exclusive Collectively Exhaustive)。原则上,每个条件语句的if/else分支都需要完全穷尽所有可能。 +一些不好的编程风格,例如: + +```cpp +// 不好的编程风格 +if (OB_SUCCESS != ret && x > 0) { + // do something +} + +if (OB_SUCCESS == ret && x < 0 ) { + // do something +} + +if (OB_SUCCESS == ret && x == 0) { + // do something +} +``` + +这样的代码不符合MECE原则,很难分析是否穷尽了所有的可能,很容易遗漏一些场景。 +如果只有一个条件,正确的写法是: + +```cpp +// 一个判断条件的正确写法 +if (cond) { + // do something +} else { + // do something +} +``` + +原则上,每个if/else分支都是完整的,即使最后一个else分支什么也不做。不过有一种情况例外,如果if条件只是一些错误判断或者参数检查,没有其它逻辑,那么,else分支可以省略。 + +```cpp +// if语句只是判断错误码,else分支可省略 +if (OB_SUCCESS != ret) { + // 处理错误 +} +``` + +如果包含两个判断条件,对比如下两种可能的写法: + +```cpp +// 两个判断条件的第一种写法(正确) +if (cond1) { + if (cond2) { + // do something + } else { + // do something + } +} else { + if (cond2) { + // do something + } else { + // do something + } +} + +// 两个判断条件的第二种写法(错误) +if (cond1 && cond2) { + // do something +} else if (cond1 && !cond2) { + // do something +} else if (!cond1 && cond2) { + // do something +} else { + // do something +} +``` + +第一种写法分为两层,第二种写法分为一层,OceanBase只允许第一种写法。当然,这里的cond1和cond2是从业务逻辑的角度说的,指的是两段独立的业务逻辑,而不是说cond1和cond2里面不能包含&&或者||运算符。例如: + +```cpp +// app_name是否为空,包含|| +if (NULL == app_name || app_name[0] == ‘\0’) { + ... +} + +// 判断table_name或者column_name是否为空,认为是一个业务逻辑 +if (NULL != table_name || NULL != column_name) { + ... +} +``` + +无论如何,每一层的if/else分支个数不超过5个。为什么选择5个?这个数字也来自麦肯锡分析法,一般来讲,同一层次的分支逻辑一般在3~5个之间,如果超过了,往往是划分不合理。 + +## 5.6 const声明 +将不会发生变化的函数参数声明为const。另外,如果函数不修改成员变量,也应该声明为const函数。 + +将参数声明为const可以避免一些不必要的错误,例如不变的参数因为代码错误被改变了。对于简单数据类型值传递,很多人对是否声明为const存在争议,因为这种情况声明const没有任何效果。 + +考虑到OceanBase已有代码大多都已经声明为const,而且这样操作起来要更加容易,因此,只要函数参数不会发生变化,统一声明为const。 + +## 5.7 函数参数 +函数参数不得超过7个,建议的顺序为:输入参数在前,输出参数在后,如果某些参数既是输入参数又是输出参数,当成输入参数处理,和其他输入参数一样放在前面,添加新的参数也需要遵守这个原则。编码的原则:代码上不相信任何人!每个函数(无论public还是private,内联函数除外)必须检查每个输入参数的合法性,强烈建议内联函数也进行这些检查(除非有严重性能问题)。所有函数(无论public还是private)都必须检查从类成员变量或者通过函数调用获得的值(例如get返回值或输出参数)的合法性,即使返回值为成功,也仍然要检查输出参数的合法性。变量(参数)检查,一个函数内只需要检查一次(如果多次调用一个或几个函数获得的值,那么每次都要检查)。这些检查包括但不限于: + +1. 指针是否为NULL,字符串是否为空 +2. 数值类型参数值是否超过值域,特别地,数组/字符串/缓冲区的下标是否越界 +3. 对象类型的参数是否有效,一般地,对象可以定义一个 `bool is_valid()` 方法(参考 `common::TableSchema`) + +如果在函数内已经做了隐式的检查,例如通过一个检查函数,那么要在变量赋值的地方予以说明。例如: + +```cpp +// 隐式检查过的变量,要在变量赋值的地方予以说明: +if (!param.is_valid() || !context.is_valid()) { + ret = OB_INVALID_ARGUMENT; + STORAGE_LOG(WARN, "Invalid argument", K(ret), K(param), K(param)); + } else { + // block_cache_非空已在前面的context.is_valid()中检查过 + ObMicroBlockCache *block_cache = context.cache_context_.block_cache_; + ... +} +``` + +使用if语句检查输入参数(函数本身)和输出参数(函数调用者)的合法性,任何时候都禁止使用 `assert`、禁止使用先前定义的 `OB_ASSERT` 宏。 +示例如下: + +```cpp +// 需要返回错误的函数 +int _func(void *ptr) +{ + int ret = OB_SUCCESS; + + if (NULL == ptr) { + // print error log + ret = OB_INVALID_ARGUMENT; + } + else { + // 执行业务逻辑 + } + return ret; +} +``` + +## 5.8 函数调用 +函数调用时应该尽量避免传入一些无意义的特殊值,例如 `NULL`,`true/false`,`0/-1`,等,而应该使用常量来替代。如果一定要传入特殊值,需要采用注释说明。 + +例如: + +```cpp +// 错误的写法 +int ret = do_something(param1, 100, NULL); +``` + +```cpp +// 正确的写法 +ObCallback *null_callback = NULL; +int ret = do_something(param1, NUM_TIMES, null_callback); +``` + +## 5.9 指针还是引用 +函数参数可以选择指针,也可以选择引用。在遵守惯用法的前提下,更多地使用引用。 + +指针参数和引用参数往往可以达到同样的效果。考虑到OceanBase编码规范中对错误判断要求比较严格,因此,更多地使用引用,减少一些冗余的错误判断代码。当然,前提是必须遵守惯用法,例如: + +1. 申请对象的方法返回往往一个指针,相应的释放方法传入的也是指针。 +2. 如果对象的成员是一个指针,相应的set_xxx传入的也是指针。 + +## 5.10 函数长度 +强制要求单个函数不超过120行。如果必须违反,请事先征得小组负责人的同意,并详细注释原因。 +大多数开源项目都会限制单个函数的行数,一般来讲,80行以上的函数往往都是不合适的。考虑到OceanBase有大量冗余的错误判断代码,限制单个函数不超过120行。如果函数过长,考虑将其分割为更加短小、易于管理的若干个函数,或者重新审视设计,修改类的结构。 + +## 5.11 总结 +1. 严格遵守函数单入口单出口。如果必须违反,请事先征得项目负责人和项目架构师的同意,并详细注释原因。 +2. 除了简单存取函数set_xxx()/get_xxx()和少量例外(如操作符重载,已有的at(i)函数,类的通用函数reset()/reuse()等),所有函数(public和private)都应该用ret返回错误码,如果set/get比较复杂或可能出错,仍然要用ret返回错误码。只能用int类型的ret变量表示错误,且ret只能表示错误(迭代器函数由于历史原因除外)。 +3. 如果多条顺序语句在做同一件事情,那么,在某些情况下可以采用精简写法。 +4. 在循环条件中判断 `OB_SUCCESS == ret`,防止错误码被覆盖等问题。 +5. 条件语句需要遵守MECE原则:各个条件之间相互独立,完全穷尽,且单个if/else的分支个数尽量不超过5个。 +6. 尽可能将函数/函数参数声明为const。 +7. 编码的原则:代码上不相信任何人!每个函数(无论public还是private,内联函数除外)必须检查每个输入参数的合法性,强烈建议内联函数也进行这些检查(除非有严重性能问题)。所有函数(无论public还是private)都必须检查从类成员变量或者通过函数调用获得的值(例如get返回值或输出参数)的合法性,即使返回值为成功,也仍然要检查输出参数的合法性。变量(参数)检查,一个函数内只需要检查一次(如果多次调用一个或几个函数获得的值,那么每次都要检查)。定义函数时,建议的顺序为:输入参数在前,输出参数在后。 +8. 禁止使用 `assert` 和 `OB_ASSERT`。 +9. 函数调用时应该尽量避免传入一些无意义的特殊值,而采用常量替代。 +10. 在遵守惯用法的前提下,更多地使用引用。 +11. 强制要求单个函数不超过120行。如果必须违反,请事先征得小组负责人的同意,并详细注释原因。 + +# 6 C&C++特性 +C++的优点是灵活,缺点也正是灵活。对于C++的很多功能,OceanBase持保守态度,本节对其中一些特性进行说明。选择这些特性的原则有二: +1. 谨慎原则:该特性比较“安全”,即使对于初学者,也没有特多的“坑” +2. 必要性:对OB的编码质量提升有“足够”的好处 + +## 6.1 智能指针与资源Guard +不允许使用智能指针,允许通过Guard类自动释放资源。 + +boost库支持智能指针,包括 `scoped_ptr`、`shared_ptr` 以及 `auto_ptr`。很多人认为智能指针能够被安全使用,尤其是 `scoped_ptr`,不过OceanBase已有代码大多都手动释放资源,且智能指针用得不好容易有副作用,因此,不允许使用智能指针。 + +允许用户手写一些Guard类,这些类的方法会申请一些资源,这些资源会在类析构的时候自动释放掉,例如 `LockGuard`,`SessionGuard`。 + +## 6.2 内存申请与释放 +要求使用内存分配器申请内存,内存释放后要立即将指针置为NULL。 + +OceanBase可用于内存分配的方法包括ob_malloc以及各种内存分配器,要求使用内存分配器申请内存,且申请时指定所属的模块。这样的好处是方便系统管理内存,如果出现内存泄露,很容易看出是哪个模块。另外,需要防止引用已经释放的内存空间,要求在free之后立刻将指针置为NULL。 + +```cpp +void *ptr = ob_malloc(100, ObModIds::OB_COMMON_ARRAY); + +// do something + +if (NULL != ptr) { + // 释放资源 + ob_free(ptr, ObModIds::OB_COMMON_ARRAY); + ptr = NULL; // free之后立即将指针置空 +} +``` + +## 6.3 字符串 +禁止使用 `std::string` 类,采用 `ObString` 代替。另外,操作C字符串时,要求使用限长字符串函数。 + +C++的 `std::string` 类使用非常方便,问题在于根本无法搞清楚其内部行为,例如拷贝,隐式转换。OceanBase要求尽量使用 `ObString`,其中用到的内存需要开发者手动管理。 +有时会使用C字符串,要注意不要使用不限制长度的字符串操作函数,包括:`strcpy/strcat/strdup/sprintf/strncpy`,而是改用对应的限长字符串操作函数:`strncat/strndup/snprintf/memcpy`。可以使用 `strlen`,用于获取字符串的长度。之所以不用 `strncpy`,那么因为如果传入buffer不够时不会自动'\0',且存在性能问题,需要替换为 `memcpy/snprintf`。 + +## 6.4 数组/字符串/缓冲区访问 +函数把数组/字符串/缓冲区作为参数传递时,必须同时传递数组/字符串/缓冲区的长度。访问数组/字符串/缓冲区的内容时,必须检查下标是否越界。 + +## 6.5 友元 +只能在同一个文件中使用友元,如果必须违反,请事先征得小组负责人的同意,并详细注释原因。将单元测试类声明为友元可以例外,但需谨慎使用。 + +通常将友元定义在同一个文件中,避免代码阅读者跑到其它文件中查找其对某个类私有成员的使用。经常用到友元的场景包括: + +1. 迭代器:往往会将迭代器类声明为友元,例如 `ObQueryEngine` 将 `ObQueryEngineIterator` 声明为友元(`friend class ObQueryEngineIterator`)。 +2. 工厂模式:例如将 `ObFooBuilder` 声明为 `ObFoo` 的友元,以便 `ObFooBuilder` 访问 `ObFoo` 的内部状态。 + +某些情况下,为了提高测试覆盖率,将一个单元测试类声明为待测类的友元会很方便。不过,需要谨慎对待这种做法。大部分情况下,我们应该通过public函数的各种输入组合间接地测试private函数,否则,这些单元测试代码将会难以维护。 + +## 6.6 异常 +**禁止使用C++异常。** + +某些编程语言鼓励使用异常,例如Java。异常确实会使得写代码比较方便,但是仅仅在写代码阶段,后续调试和改bug将会很不方便。异常使得程序控制流变得更加复杂,且容易忘记捕获某些异常,因此,禁止使用,采用ret错误码返回错误。 + +## 6.7 运行时类型识别 + +**禁止使用运行时类型识别(RTTI)。** + +运行时类型识别往往意味着设计本身有问题,如果一定要使用,这通常说明要重新考虑类的设计。 + +## 6.8 类型转换 +使用 `static_cast<>` 等C++类型转换,禁止使用类似 `int y = (int) x` 的C强制类型转换。 +C++风格的类型转换包括: + +1. static_cast:和C风格相似可以做值的强制转换,或者指针的子类到父类的明确的向上转换。 +2. const_cast:移除const属性。 +3. reinterpret_cast:指针类型和整数或其它指针间不安全的相互转化,使用时需要谨慎。 +4. dynamic_cast:除了测试代码,禁止使用。 + +需要谨慎使用 `const_cast`。特别地,对于声明为const的入参,原则上禁止使用 `const_cast` 去掉const。 + +`const_cast`会造成代码阅读者的认知困难:对一个函数的const 入参,分析代码逻辑的时候会认为这个参数是函数外生成的,不会在函数内部修改;而使用 `const_cast` 会破坏这个假设,导致代码阅读者无法注意到函数内部对于 const 入参的修改。例如,如下代码片段中的 `const_cast` 是禁止使用的。 +```cpp +int foo(const char* bar, int64_t len) +{ + ... + memcpy(const_cast(bar), src, len); + ... + return OB_SUCCESS; +} +``` + +## 6.9 输出 +**尽量采用to_cstring输出。** + +原则上,每个支持打印的类都需要实现to_string,to_cstring使用示例如下: +```cpp +FILL_TRACE_LOG("cur_trans_id=%s", to_cstring(my_session->get_trans_id())); +FILL_TRACE_LOG("session_trans_id=%s", to_cstring(physical_plan->get_trans_id())); +``` + +## 6.10 整型 +返回的ret错误码使用int,函数参数和循环次数尽量使用int64_t。其它情况使用指定长度的有符号数,例如int32_t,int64_t。避免使用无符号数,少数惯用法除外。 + +函数参数和循环次数之所以尽量使用int64_t,是为了避免函数调用和循环语句大量的数据类型转化。当然,惯用法可以除外,例如端口为int32_t。而在struct这样的结构体中,往往会有8字节对齐或者节省内存的需求,因此,可以使用指定长度的有符号数。 + +除了bit set或者编号(例如table_id)这样的惯用法以外,都应该避免使用无符号数。无符号数可能带来一些隐患,例如: +```cpp +for (unsigned int i = foo.length() – 1; i >= 0; --i) +``` + +上述代码永远不会终止。 + +对于编号,目前的代码有的采用0作为非法值,有的采用 `MAX_UINT64` 作为非法值,以后统一规定:0以及 `MAX_UINT64` 都是非法值,且在utility中提供内联函数 `is_valid_id` 用于检查。另外,统一将编号值初始化为宏 `OB_INVALID_ID`,宏 `OB_INVALID_ID` 的初始值调整为0。 + +## 6.11 sizeof +**尽量使用sizeof(var_name)代替sizeof(type)。** + +这是因为,如果var_name的类型发生变化,`sizeof(var_name)` 会自动同步,而 `sizeof(type)` 不会,这可能会带来一些隐患。 + +```cpp +ObStruct data; +memset(&data, 0, sizeof(data)); // 正确的写法 +memset(&data, 0, sizeof(ObStruct)); // 错误的写法 +``` + +需要注意的是,不要使用sizeof计算字符串的长度,而改用strlen。例如: + +```cpp +Char *p = “abcdefg”; +int64_t nsize = sizeof(p); // sizeof(p)表示指针大小,在 64位机上等于8 +``` + +## 6.12 0与nullptr +整数用0,实数用0.0,指针用nullptr(替代之前的NULL),字符串用'\0'。 + +## 6.13 预处理宏 +除了已有的宏之外,不得定义新的宏,以内联函数、枚举和常量代替。如果必须定义新的宏,请事先征得小组负责人的同意,并详细注释原因。 + +宏可以做一些其它技术无法实现的事情,例如字符串化(stringifying,使用#),连接(concatenation,使用##)。宏往往用于输出和序列化,例如common/ob_print_utils.h,rpc相关类,然而,很多时候,可以使用其它方式代替宏:宏内联效率关键代码可以通过内联函数替代,宏存储常量可以使用const变量替代。 + +判断的原则是:除了输出和序列化,只要能不用宏,尽量不要用宏。 + +## 6.14 Boost与STL +STL中只允许使用头文件定义的算法类函数,如std_sort,禁止使用其它STL或boost功能。如果必须违反,请事先征得项目负责人和项目架构师的同意,并详细注释原因。 + +OceanBase对boost和STL这样的库持保守态度,我们认为正确地编写代码的重要性远远高于方便地编写代码。除了STL 定义的算法类函数,其它功能都不应该使用。 + +## 6.15 auto +**是什么** + +声明变量的时候免去具体类型,由编译器根据初始化表达式自动推导类型。 + +**例子** +```cpp +auto i = 42; // i is an int +auto l = 42LL; // l is an long long +auto p = new foo(); // p is a foo* +``` + +是否允许 +**禁止。** + +虽然可以使得一些模板类型的声明更短,但是我们希望类型的声明符合使用者的意图。比如上面的例子1、2中,更应该显式声明是什么类型。 + +## 6.16 Range-based for loops +**是什么** + +新的for循环语法,用来方便地对提供begin(), end()的容器进行遍历。 + +**例子** + +```cpp +for(const auto& kvp : map) { + std::cout << kvp.first << std::endl; +} +``` + +是否允许 + +**禁止。** + +这个特性只是一个语法糖。之前OB的代码中已经大量使用了我们自己定义的FOREACH宏,可以达到类似的效果。 + +## 6.17 Override and final +**是什么** + +override用来表明一个虚函数是对基类中虚函数的重载;final表明某个虚函数不能被派生类重载。 + +**例子** + +```cpp +class B +{ +public: + virtual void f(short) {std::cout << "B::f" << std::endl;} + virtual void f2(short) override final {std::cout << "B::f2" << std::endl;} +}; +class D : public B +{ +public: + virtual void f(int) override {std::cout << "D::f" << std::endl;} +}; +class F : public B +{ +public: + virtual void f2(int) override {std::cout << "D::f2" << std::endl;} // compiler error +}; +``` + +是否允许 +**允许。** override和final不光允许使用,而且强烈推荐使用,在能用的地方都要加上。 + +根据之前的经验,OB中虚函数重载漏了const导致重载错误的错误层出不穷。要求新代码中,所有重载都要加上override,以避免这种错误重载情况发生。 + +除了用于虚函数,当一个类加上final关键字的时候,表示他不能被进一步派生,有利于编译器优化。当这样的类无父类的时候,析构函数可以不需要加virtual。 + +```cpp +class ObLinearRetry final: public ObIRetryPolicy +{ + // ... +}; + +class ObCData final +{ + ~ObCData(); +} +``` + +## 6.18 Strongly-typed enums +**是什么** + +传统的枚举类型有太多缺点,并不是真正的 类型 。比如会被隐式地转换为整型;枚举值和定义其类型的地方位于同一层作用域。 + +**例子** + +```cpp +enum class Options {None, One, All}; +Options o = Options::All; +``` + +是否允许 +**允许。**原来的枚举类型就是一个C++语言的BUG。新的枚举类型让编译器的检查更加严格,且使用新的关键字定义,和原来的enum不冲突。 + +## 6.19 Lambdas +**是什么** + +从函数式编程中借鉴的概念,用来方便地书写匿名函数。 + +**例子** + +```cpp +std::function lfib = [&lfib](int n) {return n < 2 ? 1 : lfib(n-1) + lfib(n-2);}; +``` + +是否允许 +**禁止。**lambda语法新奇,让C代码看起来像一种新语言,且多数人对函数式编程的理解不足,学习代价较大。不符合原则(1)。另外,lambda本质上等价于定义一个functor,是一个语法糖,其并没有增加C的抽象能力。不符合原则(2)。 + +## 6.20 non-member begin() and end() +**是什么** + +全局函数 `std::begin()` 和 `std::end()`,用来方便地抽象对容器的操作。 + +**例子** +```cpp +int arr[] = {1,2,3}; +std::for_each(std::begin(arr), std::end(arr), [](int n) {std::cout << n << std::endl;}); +``` + +是否允许 +**禁止。**这个特性主要是让STL更加好用,不过OB禁止使用STL容器。 + +## 6.21 static_assert and type traits +**是什么** + +编译器支持的编译期间assert,以及编译期约束检查。 + +**例子** + +```cpp +template +class Vector +{ + static_assert(Size < 3, "Size is too small"); + T _points[Size]; +}; +``` +是否允许 +**允许。**虽然OB代码已经自己定义了 `STATIC_ASSERT`,但是其只是对编译器检查的模拟,报错不友好。而 `type_traits`(参考[4])给模板的使用带来很大好处。 + +## 6.22 Move semantics +**是什么** + +`move constructor` 和 `move assignment operator` 是C++11最重要的一个新特性。伴随着它,引入了rvalue的概念。 移动 的语义可以让很多容器的实现变的比以前高效很多。 + +**例子** + +```cpp +// move constructor +Buffer(Buffer&& temp): +name(std::move(temp.name)), +size(temp.size), +buffer(std::move(temp.buffer)) +{ + temp._buffer = nullptr; + temp._size = 0; +} +``` +是否允许 + +**禁止。**禁止它可能带来一些争论。主要基于以下考虑: + +1. OB不使用STL容器,所以标准库使用移动语义的优化对我们没有带来好处。 +2. Move semantic和rvalue的语义比较复杂,容易引入坑 +3. 用它改造OB现有某些容器,确实可以带了性能的提升。但是,OB的内存管理方式已经使得移动语义的用武之地变小了。很多时候,我们在实现的时候已经做了优化,在容器里只保存指针,而不是大对象。 + +建议在其他C++11特性熟悉一段时间以后,下一次修订编码规范的时候再考虑。 + +## 6.23 constexpr +**是什么** + +更加标准化的编译时常量表达式计算支持,不再需要使用各种模板的奇技淫巧来达到编译期计算的效果。 + +**例子** + +```cpp +constexpr int getDefaultArraySize (int multiplier) +{ + return 10 * multiplier; +} +int my_array[ getDefaultArraySize( 3 ) ]; +``` + +是否允许 +**允许。**常量对于编译优化总是更加友好的。在上面的例子中,还避免了宏的使用。此外,constexpr支持浮点数计算,这时用static const无法代替的。 + +## 6.23 Uniform initialization syntax and semantics +**是什么** + +任意类型变量在任意上下文的初始化,都可以使用统一的{}语法。 + +**例子** + +```cpp +X x1 = X{1,2}; +X x2 = {1,2}; // the = is optional +X x3{1,2}; +X* p = new X{1,2}; +``` + +是否允许 +**禁止。**语法上更加统一,但也没有带来什么显著的好处。同时,会显著影响OB代码的风格,影响可读性。 + +## 6.24 Right Angle Brackets +**是什么** + +修复原C中的一个常见语法问题。原来C定义模板的模板嵌套的时候,结尾的>>之间必须用空格分离,现在不用了。 + +**例子** + +```cpp +typedef std::vector> Flags; +``` +是否允许 + +**允许。** + +## 6.25 Variadic templates +**是什么** + +可变参数模板。 + +**例子** + +```cpp +template +void func(const Arg1& arg1, const Args&... args) +{ + process( arg1 ); + func(args...); // note: arg1 does not appear here! +} +``` +是否允许 +**允许。**对于模板编程来说这是一个很关键的特性。因为没有变长模板参数,OB的一些基础库如 to_string , to_yson, RPC框架,日志库等都需要用一些奇技淫巧结合宏来实现。且类型更加安全。 + +## 6.26 Unrestricted unions +**是什么** + +之前Union中不能包含有构造函数的类作为成员,现在可以了。 + +**例子** + +```cpp +struct Point { + Point() {} + Point(int x, int y): x(x), y(y) {} + int x, y; +}; + +union U { + int z; + double w; + Point p; // Illegal in C03; legal in C11. + U() {} // Due to the Point member, a constructor definition is now needed. + U(const Point& pt) : p(pt) {} // Construct Point object using initializer list. + U& operator=(const Point& pt) { new(&p) Point(pt); return *this; } // Assign Point object using placement 'new'. +}; +``` + +是否允许 + +**允许。**OB的代码中有多处因为这个限制而不得不定义了冗余的域,或者用很trick的方法绕过(定义char数组占位)。悲惨的例子见sql::ObPostExprItem。 + +## 6.27 Explicitly defaulted and deleted special member functions +**是什么** + +之前C++最让人困扰的地方之一就是编译器隐式自动帮你生成构造函数,拷贝构造函数,赋值运算符,析构函数等。现在可以显式地要求或者禁止他们。 + +**例子** + +```cpp +struct NonCopyable { + NonCopyable() = default; + NonCopyable(const NonCopyable&) = delete; + NonCopyable& operator=(const NonCopyable&) = delete; +}; +struct NoInt { + void f(double i); + void f(int) = delete; +}; +``` +是否允许 + +**允许。**这个特性就像是为OB量身定制的;禁止某个函数的功能也很有用。 + +## 6.28 Type alias (alias declaration) +**是什么** + +使用新的alias declration语法可以定义类型的别名,和之前的typedef类似;而且,还可以定义别名模板。 + +**例子** + +```cpp +// C++11 +using func = void(*)(int); +// C++03 equivalent: +// typedef void (*func)(int); +template using ptr = T*; +// the name 'ptr' is now an alias for pointer to T +ptr ptr_int; +``` +是否允许 + +**禁止。**暂时没有遇到别名模板的需求,而非模板的别名用typedef可以达到相同的作用。 + +## 6.29 总结 +1. 不允许使用智能指针,允许通过Guard类自动释放资源。 +2. 要求使用内存分配器申请内存,内存释放后要立即将指针置为NULL。 +3. 禁止使用std::string类,采用ObString代替。另外,操作C字符串时,要求使用限长字符串函数。 +4. 作为参数传递数组/字符串/缓冲区时必须同时传递长度,读写数组/字符串/缓冲区内容时要检查下标是否越界。 +5. 只能在同一个文件中使用友元,如果必须违反,请事先征得小组负责人的同意,并详细注释原因。将单元测试类声明为友元可以例外,但需谨慎使用。 +6. 禁止使用C++异常。 +7. 禁止使用运行时类型识别(RTTI)。 +8. 使用static_cast<>等C++类型转换,禁止使用类似 `int y = (int) x` 的C强制类型转换。 +9. 尽量采用to_cstring输出。 +10. 返回的ret错误码使用int,函数参数和循环次数尽量使用int64_t。其它情况使用指定长度的有符号数,例如int32_t,int64_t。尽量避免使用无符号数。 +11. 尽量使用sizeof(var_name)代替sizeof(type)。 +12. 整数用0,实数用0.0,指针用NULL,字符串用’\0’。 +13. 除了已有的宏之外,不得定义新的宏,以内联函数、枚举和常量代替。如果必须违反,请事先征得小组负责人的同意,并详细注释原因。 +14. 除了STL中头文件定义的算法类函数外,禁止使用STL及boost。如果必须违反,请事先征得项目负责人和项目架构师的同意,并详细注释原因。 + +# 7 命名规则 +## 7.1 通用规则 +函数命名、变量命名、文件命名应该具有描述性,不要过度缩写,类型和变量应该是名词,函数可以用“命令性”动词。 + +标识符命名有时候会使用一些通用的缩写,但不允许使用过于专业或不大众化的。例如我们可以使用如下范围: + +1. temp 可缩写为tmp ; +2. statistic 可缩写为stat ; +3. increment 可缩写为inc ; +4. message 可缩写为msg ; +5. count可缩写为 cnt; +6. buffer可缩写为buf,而不是buff; +7. current可缩写为cur,而不是curr; + +使用缩写时,需要考虑是否每个项目组成员都能理解。如果不太确定,避免使用缩写。 + +类型和变量一般为名词,例如,ObFileReader,num_errors。 + +函数名通常是命令性的,例如open_file(),set_num_errors()。 + +## 7.2 文件命名 +自描述良好的全小写单词组成,每个单词之间使用'_'分割,例如 +ob_update_server.h以及ob_update_server.cpp。 + +.h文件和.cpp文件互相对应,如果模板类代码较长,可以放到.ipp文件中,例如ob_vector.h和ob_vector.ipp。 + +## 7.3 类型命名 +使用自描述良好的单词组成。为了和变量区分,建议使用单词首字母大写,中间无分隔符的方式。嵌套类不需要加“Ob”前缀,其它类都需要加“Ob”前缀。例如: + +```cpp +// class and structs +class ObArenaAllocator +{ + ... + struct ObUpsInfo + { ... } +}; + + // typedefs +typedef ObStringBufT<> ObStringBuf; + +// enums +enum ObPacketCode +{ ... }; + +// inner class +class ObOuterClass +{ +private: + class InnerClass + { ... }; +}; +``` + +接口类需要前面加“I”修饰符,其它类都不要加,例如: + +```cpp +class ObIAllocator +{ ... }; +``` + +## 7.4 变量命名 +### 7.4.1 类内变量命名 +自描述良好的全小写单词组成,单词之间使用'_'分隔,为了避免与其他变量混淆,要求采用后端加入'_'的方式区分,例如: + +```cpp +class ObArenaAllocator +{ +private: + ModuleArena arena_; +}; + +struct ObUpsInfo +{ + common::ObServer addr_; + int32_t inner_port_; +}; +``` + +### 7.4.2 普通变量命名 +自描述良好的全小写单词组成,单词之间使用'_'分隔。 + +### 7.4.3 全局变量命名 +除了已有的全局变量外,不得使用新的全局变量。如果必须违反,请事先征得小组负责人的同意,并详细注释原因。全局变量由自描述良好的全小写单词组成,单词之间使用'_'分隔,为标注全局性质,要求在前端加入'g_'修饰符。例如: + +```cpp +// globle thread number +int64_t g_thread_number; +``` + +## 7.5 函数命名 +### 7.5.1 类别内函数命名 +自描述良好的全小写单词组成,单词之间使用'_'分隔,例如: + +```cpp +class ObArenaAllocator +{ +public: + int64_t used() const; + int64_t total() const; +}; +``` + +### 7.5.2 存取函数命名 +存取函数的名称需要和类成员变量对应,如果类成员变量为xxx,那么存取函数分别为set_xxx和get_xxx。 + +### 7.5.3 普通函数命名 +自描述良好的全小写单词组成,单词之间使用'_'分隔。 + +## 7.6 常量命名 +所有编译时常量, 无论是局部的, 全局的还是类中的, 要求全部使用大写字母组成,单词之间以'_'分割。例如: + +```cpp +static const int NUM_TEST_CASES = 6; +``` + +## 7.7 宏命名 +尽量避免使用宏,宏命名全部使用大写字母组成,单词之间以'_'分割。注意,定义宏时必须对参数加括号。例如: + +```cpp +// 正确的写法 +#define MAX(a, b) (((a) > (b)) ? (a) : (b)) +// 错误的写法 +#define MAX(a, b) ((a > b) ? a : b) +``` + +## 7.8 注意事项 +有几点比较容易忘记,说明如下: +1. 尽量不要采用缩写,除非缩写名足够清晰且能被项目组成员广泛接受。 +2. 除了接口类名称需要加I修饰意外,其它类、结构体、枚举类型都不需要修饰符 +3. struct内的变量名也需要加下划线 + +# 8 排版风格 +## 8.1 代码缩进 +不要使用Tab键进行缩进,可以用空格替代,不同的编码工具都可以进行设置,要求使用两个空格缩进(4个空格缩进在单行代码比较长的时候会显得有些不够紧凑)。 + +## 8.2 空行 +尽量减少不必要的空行,只有代码逻辑明显分为多个部分时才这么做。 + +函数体内部可视代码决定,一般来讲,只有当代码逻辑上分成多个部分时,各个部分之间才需要增加一个空行。 + +下面这些代码都不应该有空行: + +```cpp +// 函数头、尾不要有空行 +void function() +{ + int ret = OB_SUCCESS; + +} + +// 代码块头、尾不要有空行 +while (cond) { + // do_something(); + +} +if (cond) { + + // do_something() +} +``` + +下面的空行是合理的。 + +```cpp +// 函数初始化和业务逻辑是两个部分,中间有一个空行 +void function(const char *buf, const int64_t buf_len, int64_t &pos) +{ + int ret = OB_SUCCESS; + int64_t ori_pos = pos; + if (NULL == buf || buf_len <= 0 || pos >= buf_len) { + // print error log + ret = OB_INVALID_ARGUMENT; + } else { + ori_pos = pos; + } + + // 执行业务逻辑 + return ret; +} +``` + +## 8.3 行长度 +每行长度不得超过100个字符,一个汉字相当于两个字符。 + +100个字符是单行的最大值,如下几种情况可以提高最大限制到120个字符: + +1. 如果一行注释包含了超过100字符的命令或者URL。 +2. 包含长路径。 + +## 8.4 函数声明 +返回类型和函数名在同一行, 参数也尽量放在同一行。 +函数看上去像这样: + +```cpp +int ClassName::function_name(Type par_name1, Type par_name2) +{ + int ret = OB_SUCCESS; + do_something(); + ... + return ret; +} +``` + +如果同一行文本较多,容不下所有参数,可以将参数拆分到多行,每行一个参数: + +```cpp +int ClassName::really_long_function_name(Type par_name1, + Type par_name2, Type par_name3) // 空4格 +{ + int ret = OB_SUCCESS; + do_something(); + ... + return ret; +} +``` + +也可以将每个参数单独放一行,后面每个参数与第一个参数对齐,如下: + +```cpp +// 后面的参数和第一个参数对齐 +int ClassName::really_long_function_name(Type par_name1, +Type par_name2, // 和第一个参数对齐 +Type par_name3) +{ + int ret = OB_SUCCESS; + do_something(); + ... + return ret; +} +``` + +如果连第一个参数都放不下: +```cpp +// 每个参数另起一行,空4格 +int ClassName::really_really_long_function_name( + Type par_name1, // 空4格 + Type par_name2, + Type par_name3) +{ + int ret = OB_SUCCESS; + do_something(); + ... + return ret; +} +``` + +注意如下几点: +- 返回值总是和函数名在同一行; +- 左圆括号总是和函数名在同一行; +- 函数名和左圆括号间没有空格; +- 圆括号与参数间没有空格; +- 左大括号总是单独位于函数的第一行(另起一行); +- 右大括号总是单独位于函数最后一行; +- 函数声明和实现处的所有形参名称必须保持一致; +- 所有形参应尽可能对齐; +- 缺省缩进为 2 个空格; +- 换行后的参数保持 4 个空格的缩进; +- 如果函数声明成const, 关键字 const 应与最后一个参数位于同一行 + +有些参数没有用到,在函数定义时将这些参数名注释起来: + +```cpp +// 正确的写法 +int ObCircle::rotate(double /*radians*/) +{ +} +``` + +```cpp +// 错误的写法 +int ObCircle::rotate(double) +{ +} +``` + +## 8.5 函数调用 +尽量放在同一行,如果放不下,可以切成多行,切分方式与函数声明类似。 + +函数调用的形式往往是这样的(左圆括号后和右圆括号前都不要留空格): + +```cpp +int ret = function(argument1, argument2, argument3); +``` + +如果切分成多行,可以将后面的参数拆分到下一行,如下: +```cpp +int ret = really_long_function(argument1, + argument2, argument3); // 空4格 +``` + +也可以将每个参数单独放一行,后面每一行都和第一个参数对齐,如下: + +```cpp +int ret = really_long_function(argument1, + argument2, // 和第一个参数对齐 + argument3); +``` + +如果函数名太长,可以将所有的参数独立成行,如下: +```cpp +int ret = really_really_long_function( + argument1, + argument2, // 空4格 + argument3); +``` + +对于placement new,new与指针变量之间需要加一个空格,如下: + +```cpp +new (ptr) ObArray();// new与‘(’之间包含一个空格 +``` + +## 8.6 条件语句 +{和if或者else在同一行,}另起一个新行。另外,if与(之间,)与{之间,都保证包含一个空格。 +条件语句往往是这样的: + +```cpp +if (cond) { // (与cond,cond与)之间没有空格 + ... +} else { // }和else,else和{之间分别有一个空格 + ... +} +``` + +无论如何,if和else语句都需要有{和},即使该分支只有一行语句。原则上,}总是另起一行,但是有一种情况可以例外。如果else分支什么也不做,}不需要另起一行,如下: + +```cpp +if (OB_SUCCESS == (ret = do_something1())) { + ... +} else if (OB_SUCCESS == (ret = do_somethng2())) { + ... +} else { } // else分支什么也不做,}不要求另起一行 +``` + +对于比较语句,如果为=,!=,那么,需要将常量写在前面;而>,>=,<,<=,则没有这个限制。例如: +```cpp +// 正确的写法 +if (NULL == p) { + ... +} + +// 错误的写法 +if (p == NULL) { + ... +} +``` + +## 8.7 表达式 +表达式运算符与前后变量之间各有一个空格,如下: + +```cpp +a = b; // =的前后各有一个空格 +a > b; +a & b; +``` + +对于布尔表达式,如果超过了行的最大长度,需要注意断行格式。另外,复杂表达式需要用括号明确表达式的操作顺序,避免使用默认优先级。 + +断行时,逻辑运算符总是位于下一行的行首,空4格: + +```cpp +if ((condition1 && condition2) + || (condition3 && condition4) // &&操作符位于行首,空4格 + || (condition5 && condition6)) { + do_something(); +} else { + do_another_thing(); +} +``` + +如果表达式比较复杂,应该加上括号明确表达式的操作顺序。 + +```cpp +// 正确的做法 +word = (high << 8) | low; +if ((a && b) || (c && d)) { + ... +} else { + ... +} +``` + +```cpp +// 错误的做法 +word = high << 8 | low; +if (a && b || c && d) { + ... +} else { + ... +} +``` + +三目运算符尽量写成一行,如果超过一行,需要写成三行。如下: + +```cpp +// 三目运算符写成一行 +int64_t length = (0 == digit_idx_) ? digit_pos_ : (digit_pos_ + 1); + +// 三目运算符写成三行 +int64_t length = (0 == digit_idx_) + ? (ObNumber::MAX_CALC_LEN - digit_pos_ - 1) // 空4格 + : (ObNumber::MAX_CALC_LEN - digit_pos_); +``` + +```cpp +// 错误:不允许分成两行 +int64_t length = (0 == digit_idx_) ? (ObNumber::MAX_CALC_LEN – digit_pos_ - 1) + : (ObNumber::MAX_CALC_LEN – digit_pos_); +``` + +## 8.8 循环和开关选择语句 +switch语句和其中的case块都需要使用{}。另外,每个case分支必须加入break语句。即使能够确保不会走到default分支,也需要写default分支。 + +```cpp +switch (var) { +case OB_TYPE_ONE: { // 顶格 + // 相对于case空4格,相对于switch空4格 + break; + } +case OB_TYPE_TWO: { + + break; + } +default: { + // 进行错误处理; + } +} +``` + +空循环体需要写一行empty注释,而不是一个简单的分号。例如: + +```cpp +// 正确的做法 +while (cond) { + // empty +} + +for (int64_t i = 0; i < num; ++i) { + // empty +} + +// 错误的做法 +while (cond) ; +for (int64_t i = 0; i < num; ++i) ; +``` + +## 8.9 变量声明 +每行只声明一个变量,变量声明时必须初始化。在声明指针变量或参数时, (*, &) 与变量名挨着。函数类型声明时,指针或引用(*, &)也是如此。 + +```cpp +// 正确的做法 +int64_t *ptr1 = NULL; +int64_t *ptr2 = NULL; + +// 错误的做法 + +int64_t *ptr1 = NULL, ptr2 = NULL; // 错误,每行只声明一个变量 +int64_t *ptr3; // 错误,变量声明时必须初始化 +int64_t* ptr = NULL; // 错误,*与变量名挨着,而不是与数据类型挨着 + +char* get_buf(); // 错误,*与变量名挨着,而不是与数据类型挨着 +char *get_buf(); // 正确 + +int set_buf(char* ptr); // 错误,*与变量名挨着,而不是与数据类型挨着 +int set_buf(char *ptr); // 正确 +``` + +## 8.10 变量引用 +对于引用和指针,需要注意:句点(.)或箭头(->)前后不要有空格。指针(*)和地址操作符(&)之后不能有空格,地址操作符紧靠变量名。 + +```cpp +// 正确的做法 +p = &x; +x = *p; +x = r->y; +x = r.y; +``` + +## 8.11 预处理指令 +预处理指令不要缩进,从行首开始。即使预处理指令位于缩进代码块中,指令也应该从行首开始。 + +```cpp +// 正确的写法,预处理指令位于行首 +#if !defined(_OB_VERSION) || _OB_VERSION<=300 + do_something1(); +#elif _OB_VERSION>300 + do_something2(); +#endif +``` + +## 8.12 类格式 +声明次序依次是public,protected,private,这三个关键字顶格,不缩进。 + +类声明的基本格式如下: + +```cpp +class ObMyClass : public ObOtherClass // :的前后各有一个空格 +{ // {另起一个新行 +public: // 顶格 + ObMyClass(); // 相对public缩进2格 + ~ObMyClass(); + explicit ObMyClass(int var); + + int some_function1(); // 第一类功能函数 + int some_function2(); + + inline void set_some_var(int64_t var) {some_var_ = var;} // 第二类功能函数 + inline int64_t get_some_var() const {return some_var_;} + + inline int some_inline_func(); // 第三类功能函数 + +private: + int some_internal_function(); // 函数定义在前 + + int64_t some_var_; // 变量定义在后 + DISALLOW_COPY_AND_ASSIGN(ObMyClass); +}; + +int ObMyClass::some_inline_func() +{ + ... +} +``` + +类的声明次序请参考第4章声明次序一节。需要说明的是,只有实现代码为一行的inline函数可以放到类定义里面,其它inline函数放到.h文件中的类定义外面。上例中的set_some_var和get_some_var只有一行实现代码,因此放到类定义里面;some_inline_func的实现代码超过一行,需要放到类定义外面。这样的好处是使得类定义更加紧凑。 + +## 8.13 初始化列表 +构造函数初始化列表放在同一行或者按照4格缩进并排成几行,且后面的参数和第一个参数对齐。另外,如果初始化列表需要换行的话,从第一个参数就要开始换行。 + +两种可以接受的初始化列表格式: + +```cpp +// 初始化列表放在同一行 +ObMyClass::ObMyClass(int var):some_var_(var), other_var_(var+1) +{ + ... +} + +// 初始化列表放在多行,按照4格缩进 +ObMyClass::ObMyClass(int var) + :some_var_(var), + some_other_var_(var+1) // 第二个参数和第一个参数对齐 +{ + ... +} +``` + +## 8.14 命名空间 +命名空间内容不要缩进。 + +```cpp +namespace oceanbase +{ +namespace common +{ +class ObMyClass // ObMyClass不要缩进 +{ + ... +} +} // namespace common +} // namespace oceanbase +``` + +## 8.15 常量代替数字 +避免使用不易理解的数字,用有意义的标识来替代。涉及物理状态或者含有物理意义的常量,不应直接使用数字,必须用有意义的枚举或常量来代替。 + +```cpp +const int64_t OB_MAX_HOST_NAME_LENGTH = 128; +const int64_t OB_MAX_HOST_NUM = 128; +``` + +## 8.16 注意事项 + +1. if&else,for&while以及switch&case语句的 `{` 都放在行的末尾,而不是另起一行; +2. 定义类的public,protected以及private关键字空2格,注意类的声明次序。 +3. 将一行切割为多行时需要注意格式。 +4. 尽量减少不必要的空行,只有代码逻辑明显分为多个部分时才这么做。 +5. 命名空间的内容不要缩进。 + +## 9 注释 +注释是为了别人理解代码而写的,下面的规则描述了应该注释什么,注释在哪里。 +## 9.1 注释语言与风格 +注释语言要求使用英文,不能使用中文,注释风格采用//。注释的目的是为了让其它人更容易理解你的代码。 + +注释风格可以用//,也可以用/* */,除了头文件的注释,其它情况都采用//。 + +## 9.2 文件注释 +在每一个文件开头加入版权公告, 版权公告见2.2节。 + +对于关键性的算法及业务逻辑,应该在此处描述清晰,定义文件头部。 + +## 9.3 类注释 + +每个类的定义都要附带一份注释, 描述类的功能和用法。例如: + +```cpp +// memtable 迭代器:如下四个需求全部使用MemTableGetIter迭代 +// 1. [常规get/scan] 需要构造RNE的cell,和根据create_time构造mtime/ctime的cell,如果有列过滤还会构造NOP的cell +// 2. [QueryEngine的dump2text] 没有列过滤和事务id过滤,不会构造NOP,但是会构造RNE/mtime/ctime +// 3. [转储] 没有列过滤和事务id过滤,会在QueryEngine跳过空的行,不会构造RNE和NOP,但会构造mtime/ctime +// 4. [单行merge] merge前需要判断事务id过滤后是否还有数据,如果没有就不调用GetIter迭代,防止构造出RNE写回memtable;此外还需要RowCompaction保证不 +// 调整顺序,防止表示事务ID的mtime被调整到普通列的后面 +// 5. [update and return] 与常规get/scan类似,但没有事务id过滤 +class MemTableGetIter : public common::ObIterator +{ +}; +``` + +请注意在此处表明使用类需要注意的事项,尤其是是否线程安全、资源如何释放等等。 + +## 9.4 函数注释 +### 9.4.1 函数声明注释 +函数声明注释位于函数声明之前,主要描述函数声明本身而不是函数怎样完成,需要描述的内容包括: + +- 函数的输入输出. +- 如果函数分配了空间, 需要由调用者释放. +- 参数是否可以为NULL. +- 是否存在函数使用上的性能隐患. +- 函数是否是可重入的,其同步前提是什么 + +```cpp +// Returns an iterator for this table. +// Note: +// It’s the client’s responsibility to delete the iterator +// when it’s done with it. +// +// The method is equivalent to: +// ObMyIterator *iter = table->new_iterator(); +// iter->seek_to_front(); +// return iter; +ObMyIterator *get_iterator() const; +``` + +一般来讲,每个类的对外接口函数都需要注释。当然,构造函数、析构函数以及存取函数这样的自描述函数是不需要注释的。 + +如果注释需要说明输入、输出参数或者返回值,格式如下例: + +```cpp +// Gets the value according to the specified key. +// +// @param [in] key the specified key. +// @param [in] value the result value. +// @return the error code. +int get(const ObKey &key, ObValue &value); +``` + +函数可重入的注释示例如下: + +```cpp +// This function is not thread safe, but it will be called by only one xxx thread. +int thread_unsafe_func(); +``` + +### 9.4.2 函数实现注释 +如果函数实现算法比较独特或者有一些亮点,可以在.cpp文件中加入函数实现注释。例如使用的编程技巧, 实现的大致步骤, 或解释如此实现的理由, 比如说明为什么前半部分要加锁而后半部分不需要。注意此处的重点说明在于如何实现,而不是拷贝.h文件中的函数声明注释。 + +## 9.5 变量注释 +局部变量可以不写注释。成员变量和全局变量一般都要写注释,除非项目组成员公认该变量是自描述的。如果变量的某些值有特殊含义,例如NULL,-1,那么,必须在注释中说明。 + +使用良好无歧义的语言标明变量用途、使用要点、作用范围。注释可以根据行字符多少决定出现在变量定义右侧或者变量定义顶部一行,例如: + +```cpp +// 注释出现在顶部一行 +private: + // Keeps track of the total number of entries in the table. + // -1 means that we don’t yet know how many entries the table has. +int num_total_entries_; + +// 注释出现在变量右侧 +static const int NUM_TEST_CASES = 6; // the total number of test cases. +``` + +## 9.6 实现注释 +同样,你必须在函数内部实现中对业务关键点、精巧算法、可读性差的部分进行详细注释。同样可以出现在代码段顶部或者某行代码右侧。 + +```cpp +// it may fail, but the caller will retry until success. +ret = try_recycle_schema(); +``` + +注意,不要以伪码方式写注释,那样过于繁琐且价值不大。 + +## 9.7 TODO注释 +尚未实现或者未完美实现的功能,有时候我们需要加入TODO注释。所有的TODO注释必须体现工作人及完成时间,当然,如果完成时间未定,你可以明白的标注出来。例如: + +```cpp +// TODO(somebody): needs another network roundtrip, will be solved by 2014/12. +``` + +## 9.8 注意事项 +1. 注释语言可以使用英文,也可以使用中文,注释风格采用// +2. 注释往往用于描述类、函数接口以及实现关键点。鼓励多写注释,除非是自描述的代码。 +3. 一定不要忘记TODO注释。 + +# 10 多线程 +## 10.1 起线程和停线程 +1. 除了极特殊的情况, 禁止动态起线程和停线程, server一旦初始化完成,线程数就是固定的。特殊情况比如: 给server留的后门,在server所有线程都被占住的情况下增加一个工作线程。 +2. 为了保证退出时某个线程不会忙等在一个死循环上,所以循环一般都要判断stop标志。 +## 10.2 pthread_key +1. pthread_key最多只有1024个,并且这个限制不能调大,使用时需要特别注意。 +2. 如果要使用大量的线程局部变量,推荐使用线程编号做数组下标获取一个线程私有的变量。OB中封装了一个itid()函数来获取连续递增的线程编号。 + +```cpp +void *get_thread_local_variable() +{ + return global_array_[itid()]; +} +``` + +## 10.3 定时器 +不能在定时器中完成耗时过长的任务, 耗时长的任务需要提交给线程池执行。 + +## 10.4 加锁和解锁 + +1. 推荐使用Guard的方式使用锁 + +```cpp +// 作用域是整个函数 +int foo() +{ + SpinLockGuardguard(lock_); + ... +} + +// 作用域是一个子句 +while(...) { + SpinLockGuardguard(lock_); + ... +} +``` + +2. 如果锁的范围不是整个函数或某个子句,比如在函数执行中途加锁,函数退出之前解锁, 这种情况允许手工加锁和解锁: + +```cpp +int foo() +{ + int ret = OB_SUCCESS; + bool lock_succ = false; + if (OB_SUCCESS != (ret = lock_.lock())) { + lock_succ = false; + } else { + lock_succ = true; + } + // 执行了若干语句 + if (lock_succ) { + lock_.unlock(); + } + return ret; +} +``` + +## 10.5 cond/signal的标准用法 + +1. 通过tbsys封装的CThreadcond使用cond/signal +2. 禁止使用不带超时的cond_wait() +3. 按以下的惯用法使用cond/signal + +```cpp +// 等待的逻辑 +cond.lock(); +while(need_wait()) +{ + cond.wait(timeout); +} +cond.unlock(); +// 唤醒的逻辑 +cond.lock(); +cond.signal(); +cond.unlock(); +``` + +## 10.6 原子操作 +统一使用定义在ob_define.h中的宏做原子操作, 需要注意: + +1. 原子读写也需要用ATOMIC_LOAD()和ATOMIC_STORE()完成 +2. 用ATOMIC_FAA()和ATOMIC_AAF()区分fetch_and_add和add_and_fetch +3. 用ATOMIC_VCAS()和ATOMIC_BCAS()区分CAS操作返回value或bool + +## 10.7 编译器barrier +一般要使用编译器barrier的地方也需要memory barrier,并且memory barrier蕴含了编译器barrier,所以应该没有什么地方需要使用编译器barrier。 + +## 10.8 memory barrier +1. 虽然有各种memory barrier,但是我们只推荐使用full barrier。因为更精细的barrier非常容易出错,目前在OB的工程实践中也没有遇到必须用更精细的barrier才能满足性能的要求的代码。各种barrier有多复杂,可以参考这个文档:https://www.kernel.org/doc/Documentation/memory-barriers.txt +2. 原子操作自带barrier,所有一般不需要手工加barrier. +3. 如果需要手工加barrier,使用宏: +```cpp +#define MEM_BARRIER() __sync_synchronize() +``` + +## 10.9 引用计数和shared_ptr +首先,不得使用shared_ptr,因为shared_ptr只是语法糖,并没有解决我们希望使用引用计数解决的问题。 + +1. 简单来讲:多线程同时操作不同的shared_ptr是安全的,但是多线程同时操作同一个shared_ptr是不安全的。当我们考虑引用计数的时候,往往都是需要多线程操作同一个shared_ptr。 +2. 具体可以参考http://en.cppreference.com/w/cpp/memory/shared_ptr + +其次,引用计数看似简单,实际不容易实现正确。除非考虑得特别清楚,也不建议使用引用计数。 +使用引用计数,首先要考虑以下的问题: 怎么保证在对引用计数加1之前,对象没有被回收或重用? +目前OB中有2种方法使用引用计数,可以参考: + +1. 下面这种简单的场景是可以使用引用计数的: + a. 单线程构造对象,对象的初始引用计数为1 + b. 之后单线程加引用计数,并把对象传给其余的线程使用,其余的线程使用完之后减引用计数。 + c. 最后单线程决定释放对象,把引用计数减1。 + +OB中的FifoAllocator就是这种用法。 + +1. 如果不满足上面的简单场景,需要用全局锁保证安全性: + a. 在例子1中的第1步加读锁 + b. 在例子1中的第3步加写锁 + +UPS管理schema_mgr时就是这么做的 + +## 10.10 对齐 +为了避免cache false sharing,如果一个变量会被多线程频繁访问,定义变量时推荐按cache line对齐。 + +```cpp +int64_t foo CACHE_ALIGNED; +``` + +但是如果某个对象大量存在,为了节省内存,允许不按cache line对齐。 + +如果是通过动态申请内存构造的对象,需要注意至少让对象的起始地址是8字节对齐的。比如. 如果使用page_arena, 可以通过alloc_aligned()来分配8字节对齐的内存。 + +```cpp +struct_A *p = page_arena_.alloc_aligned(sizeof(*p)); +``` + +## 10.11 volatile +总的来讲,不推荐使用volatile变量, 原因参考这篇文档https://www.kernel.org/doc/Documentation/volatile-considered-harmful.txt + +改用 `ATOMIC_LOAD()/ATOMIC_STORE()` 保证对变量的读写不会被优化掉。 + +```cpp +// 错误的方法 +volatile int64_ti = 0; +x = i; +i = y; +// 推荐的做法 +int64_ti = 0; +x = ATOMIC_LOAD(&i); +ATOMIC_STORE(&i, y); +``` + +在少数情况下使用volatile依然是合理的, 比如用来指示状态,但是这个状态变化又没有严格时序上的意义:比如指示线程退出的标志变量。 + +```cpp +volatile bool stop_ CACHE_ALIGNED; +``` + +或者某些监控项。 +```cpp +volatile int64_t counter_CACHE_ALIGNED; +``` + +## 10.12 使用CAS的方法 +因为 `ATOMIC_VCAS` 在操作失败的情况下返回了*addr的最新值,所以每次重试的时候不必要再次用 `ATOMIC_LOAD` 读取。 + +比如要实现原子加1,按如下的方式使用CAS操作: + +```cpp +int64_t tmp_val = 0; +int64_told_val = ATOMIC_LOAD(addr) +while(old_val != (tmp_val = ATOMIC_VCAS(addr, old_val, old_val + 1))) +{ + old_val = tmp_val; +} +``` + +## 10.13 spin wait和PAUSE +在spin wait的循环中要加入PAUSE(), 在某些CPU上PAUSE()可以提高性能,并且一般来讲PAUSE()可以降低CPU功耗。 + +```cpp +while (need_retry()) { + PAUSE(); +} +``` + +PAUSE的作用可以看这个回答:http://stackoverflow.com/questions/12894078/pause-instruction-in-x86/12904645#12904645 + +## 10.14 临界区 +不得在临界区执行耗时较长或者复杂的操作,例如打开/关闭文件,读写文件等。 + +## 10.15 避免程序core或退出 + +数据库系统重启的时间常常以小时计,大面积的core或者退出将导致数据库服务中断,并可能被恶意攻击者利用。因此必须避免程序core或者退出,例如访问空指针指向的地址(临时修改用于定位bug除外),或者调用abort(除非收到外部指令)等。如果必须违反,请事先征得项目负责人和项目架构师的同意,并详细注释原因。 + +## 11 日志规范 +1.0版本的日志模块有两项主要改进: + +**支持多维度、细粒度的打印级别设置** + +相比之前版本只支持全局统一设定日志级别的情况,1.0版本支持语句、session、租户和全局(或server)四个不同范围的打印日志设置。其中不同范围的设置方式分别为: + + - SQL语句hint + - 设置session日志级别变量 + - 设置租户日志级别变量 + - 设置系统日志级别变量 + +另外,1.0版本还支持日志模块、子模块的概念。程序中打印日志时,需要指明该条日志所属的模块(或者模块+子模块)和该条日志所属的日志打印级别。系统支持用户为各模块、子模块的分别设置不同的打印级别。 + +**更严格的日志打印格式** + +0.5版本中存在着打印日志格式不统一,不易读的问题。例如打印值为5的变量m时,有多种不同的打印格式:”m = 5”, “m=5”, “m(5)”, +“m is 5”, “m:5” 等。新的日志模块提供用户按照key-value对的方式打印所需变量的值。 + +## 11.1 日志打印级别 +| 级别 | 面向用户 |级别定义 | +| --- | --- | --- | +| ERROR | DBA | 任何未预期的、不可恢复的、需要人工干预的错误 | +| WARN | DBA | 可预期的并且可以由程序解决的异常情况 | +| INFO(启动默认级别) | DBA | 少量对系统状态变化的标记信息。例如,添加了某用户、某表、系统进入每日合并、partition迁移等。| +| EDIAG | 研发 | Error Diagnosis,协助排查问题的诊断信息,意外的逻辑错误,比如函数参数不符合预期等,通常是OceanBase程序的BUG| +| WDIAG | 研发 | Warning Diagnosis, 协助排除故障的诊断信息、预期错误,例如函数返回失败 | +| TRACE | 研发 | 请求粒度的调试信息,例如执行一条SQL语句的不同阶段分别打印一条TRACE日志 | +| DEBUG | 研发 | 一般性的、详细的调试信息,用以跟踪系统内部的状态、数据结构等。| + +需要注意的是,DEBUG日志往往用于集成测试或者线上系统的调试,不能用来代替单元测试。 + +## 11.2 打印模块的划分(实例) +| 模块 | 子模块定义 | +| ----------- | --------------------------------------------------- | +| SQL | Parser, transformer, optimizer, executor, scheduler | +| STORAGE | TBD | +| TRANSACTION | TBD | +| ROOTSERVER | TBD | +| COMMON | TBD | +| DML | TBD | + +各模块下的子模块定义将由各组内部进一步细化。模块与子模块的定义放在文件ob_log_module.h中。 + +## 11.3 打印范围的设置 +1.0版本支持用户按照语句、session和全局(系统)范围分别设置打印级别。系统中参考的优先级为 +1. 语句 +2. session +3. 系统全局(或server)只有在前一项无设置或设置无效的情况下,系统才会参考后面的级别设置。 + +### 11.3.1 语句范围打印级别设置 +【设置格式】 + +语句hint 中加入/*+ ... log_level=[log_level_statement]...*/ + +(log_level_statement的格式参见后面章节) + +【作用范围】 + +整个语句的处理、执行过程,包括语句分析、优化、执行等等。语句执行结束后,该设置自动失效。 + +### 11.3.2 session范围打印级别设置 + +【设置格式】 + +sql> set @@session.log_level = '[log_level_statement]'; + +【作用范围】 + +自设置起到该session结束前。 + +### 11.3.3 租户范围打印级别设置 + +【设置格式】 + +sql>set @@global.log_level ='[log_level_statement]'; + +【作用范围】 + +从用户设置开始对所有用户session生效,直至用户所有session退出。 + +### 11.3.4 系统(或server)范围打印级别设置 +【设置格式】 + +sql>alter system set log_level = '[log_level_statement]{,server_ip=xxx.xxx.xxx.xxx}'; + +【作用范围】 + +当用户指定server_ip时,该次设定只对该server生效,并直到该server退出或重启前一直有效;当用户没有指定server_ip时,设定对整个系统内所有server生效,并保持到整个系统reboot之前(新上线的server也需服从该设定)。 + +### 11.3.5 log_level_statement格式 +``` +log_level_statement = +mod_level_statement {, mod_level_statement } +mod_level_statement = +[mod[.[submod|*]]:][ERROR|WARNING|INFO|TRACE|DEBUG] +``` + +其中mod和submod的定义参见12.2节。没有指明mod或submod的情况下,该设定对所有mod生效。如果多项mod_level_statement设定有冲突时,以最后一个有效的设定为准。 + +用户设定不保证原子性:例如,在存在多项设定时,如果第n项设定不成功(语法错误或是模块不存在),如果是session或是系统级设置则语句报错,但之前生效项不回滚,如果是在语句hint中发生,则不报错且之前生效项不回滚。 + +## 11.4 日志格式的统一 + +1.0版本统一使用“key=value“的格式打印日志。由日志模块统一提供类似如下接口: + +```cpp +OB_MOD_LOG(mod, submod, level, "info_string", var1_name, var1, var2, 2.3, current_range, range, ...); +``` +相应的打印信息为 + +``` +[2014-10-09 10:23:54.639198] DEBUG ob_tbnet_callback.cpp:203 [12530][Ytrace_id] info_string( +var1_name=5, var2=2.3, current_range= "table_id:50,(MIN;MAX)" ) +``` + +其中info_string为该条日志的主要信息总结,应简洁、明了、易读。避免出现"operator +failed"等无信息量的字符串。 + +每一行的日志头(包括文件名、行号等信息)由日志打印模块自动生成。为方便使用,日志模块头文件(ob_log_module.h)还将提供以模块、子模块为单位定义的宏,使在某一文件或是文件夹下的程序打印语句更为简洁,例如: + +```cpp +#define OB_SQL_PARSER_LOG(level,...) OB_MOD_LOG(sql, parser, level...) +``` + +打印变量的名称的选择应考虑不同场合的需求。如果不使用变量名本身,应考虑系统中是否已有相同含义的变量名在使用(如版本号打印为”data_version”还是”version”,还是”data version”应该尽量统一),方便日后的调试和监控。 + +遇有因操作不成功而返回的地方必须打印日志,且必须打印该错误码。 + +新的日志由于支持模块、范围的设置,对打印信息的过滤将更加有效,原则上必要的debug日志信息应进一步丰富,以方便日后的排错和调试。 + +# 12 编码约束小结 +## 12.1 作用域 +1. 命名空间和目录对应,禁止使用匿名命名空间,.h文件中禁止使用using指令,只允许使用using声明。 +2. 嵌套类适合用在只被外部类使用的场景,建议在.h文件中前置申明,在.cpp文件中实现,尽量不要用public。 +3. 除了已有的全局变量和全局函数外,不得增加新的全局变量和全局函数。如果必须违反,请事先征得小组负责人的同意,并详细注释原因。 +4. 局部变量在语句块开头处声明,强制要求简单变量声明时就初始化。 +5. 禁止在循环体内声明非简单变量。如果必须违反,请事先征得小组负责人的同意,并详细注释原因。 +6. 资源管理遵守“谁申请谁释放”的原则。如果需要释放资源,在函数返回前或者最外层else分支的末尾释放。因此如果需要恢复输入参数,在函数返回前恢复。如果必须违反,请事先征得小组负责人的同意,并详细注释原因。 + +## 12.2 类 +1. 构造函数只做trival的初始化工作,每个类都需要定义至少一个构造函数,带有虚函数或者子类的析构函数声明为virtual。 +2. 为了避免隐式类型转换,需要将单参数构造函数声明为explicit。 +3. 原则上不得使用拷贝构造函数(已经定义使用的基础类除外)。如果必须违反,请事先征得小组负责人的同意,并详细注释原因。 +4. 使用DISALLOW_COPY_AND_ASSIGN避免拷贝构造函数、赋值操作滥用; +5. 类重置使用reset,重用使用reuse,禁止使用clear。 +6. 需要确保初始化所有成员,且成员变量初始化顺序和定义顺序保持一致。 +7. 仅在只有数据时使用struct,其他情况一概使用class。 +8. 每个类包含的通用函数都必须采用标准原型,序列化/反序列化函数必须使用宏实现。 +9. 优先考虑组合,只有在“是一个”关系时使用继承。避免私有继承和多重继承,多重继承使用时,要求除一个基类含有实现外,其他基类都是纯接口类。 +10. 除了已有的容器类、自定义类型以及少量全局基础类以外,不允许重载操作符(简单的结构的赋值操作除外)。如果必须违反,请事先征得小组负责人的同意,并详细注释原因。 +11. 声明次序:public->protected->private。 + +## 12.3 函数 +1. 严格遵守函数单入口单出口。如果必须违反,请事先征得项目负责人和项目架构师的同意,并详细注释原因。 +2. 除了简单存取函数set_xxx()/get_xxx()和少量例外(如操作符重载,已有的at(i)函数,类的通用函数reset()/reuse()等),所有函数(public和private)都应该用ret返回错误码,如果set/get比较复杂或可能出错,仍然要用ret返回错误码。只能用int类型的ret变量表示错误,且ret只能表示错误(迭代器函数由于历史原因除外)。 +3. 如果多条顺序语句在做同一件事情,那么,在某些情况下可以采用精简写法。 +4. 在循环条件中判断OB_SUCCESS == ret,防止错误码被覆盖等问题。 +5. 条件语句需要遵守MECE原则:各个条件之间相互独立,完全穷尽,且单个if/else的分支个数尽量不超过5个。 +6. 尽可能将函数/函数参数声明为const。 +7. 编码的原则:代码上不相信任何人!每个函数(无论public还是private,内联函数除外)必须检查每个输入参数的合法性,强烈建议内联函数也进行这些检查(除非有严重性能问题)。所有函数(无论public还是private)都必须检查从类成员变量或者通过函数调用获得的值(例如get返回值或输出参数)的合法性,即使返回值为成功,也仍然要检查输出参数的合法性。变量(参数)检查,一个函数内只需要检查一次(如果多次调用一个或几个函数获得的值,那么每次都要检查)。定义函数时,建议的顺序为:输入参数在前,输出参数在后。 +8. 禁止使用assert和OB_ASSERT。 +9. 函数调用时应该尽量避免传入一些无意义的特殊值,而采用常量替代。 +10. 在遵守惯用法的前提下,更多地使用引用。 +11. 强制要求单个函数不超过120行。如果必须违反,请事先征得小组负责人的同意,并详细注释原因。 +## 12.4 C&C++特性 +1. 不允许使用智能指针,允许通过Guard类自动释放资源。 +2. 要求使用内存分配器申请内存,内存释放后要立即将指针置为NULL。 +3. 禁止使用std::string类,采用ObString代替。另外,操作C字符串时,要求使用限长字符串函数。 +4. 作为参数传递数组/字符串/缓冲区时必须同时传递长度,读写数组/字符串/缓冲区内容时要检查下标是否越界。 +5. 只能在同一个文件中使用友元,如果必须违反,请事先征得小组负责人的同意,并详细注释原因。将单元测试类声明为友元可以例外,但需谨慎使用。 +6. 禁止使用C++异常。 +7. 禁止使用运行时类型识别(RTTI)。 +8. 使用static_cast<>等C++类型转换,禁止使用类似int y = (int) x的C强制类型转换。 +9. 尽量采用to_cstring输出。 +10. 返回的ret错误码使用int,函数参数和循环次数尽量使用int64_t。其它情况使用指定长度的有符号数,例如int32_t,int64_t。尽量避免使用无符号数。 +11. 尽量使用sizeof(var_name)代替sizeof(type)。 +12. 整数用0,实数用0.0,指针用NULL,字符串用’\0’。 +13. 除了已有的宏之外,不得定义新的宏,以内联函数、枚举和常量代替。如果必须违反,请事先征得小组负责人的同意,并详细注释原因。 +14. 除了STL中头文件定义的算法类函数外,禁止使用STL及boost。如果必须违反,请事先征得项目负责人和项目架构师的同意,并详细注释原因。 +## 12.5 其他 +- 不得在临界区执行耗时较长或者复杂的操作,例如打开/关闭文件,读写文件等。 +- 不得使用shared_ptr,严格限制引用计数的使用。 +- 必须避免程序core或者退出,例如访问空指针指向的地址(临时修改用于定位bug除外),或者调用abort(除非收到外部指令)。如果必须违反,请事先征得项目负责人和项目架构师的同意,并详细注释原因。 diff --git a/docs/docs/zh/container.md b/docs/docs/zh/container.md new file mode 100644 index 000000000..fced062c9 --- /dev/null +++ b/docs/docs/zh/container.md @@ -0,0 +1,659 @@ +--- +title: 基础数据结构 +--- + +# 介绍 + +C++ STL提供了很多很方便的容器,比如vector、map、unordered_map等,由于OceanBase编程风格与内存控制等原因,在OceanBase中禁止使用STL的容器。OceanBase 提供了一些容器实现,包括数组、链表、HashMap等,本篇文档会对这些容器做一些介绍。 + +> 本篇文档假设你对C++ STL 容器已经有了一定的理解。 + +> pair不属于容器,因此可以使用。 + +> 由于历史遗留原因,OceanBase中包含了一些不再建议使用但是没有删除的容器代码。 + +# 字符串 +OceanBase 提供的字符串类是 ObString。代码参考 ob_string.h。 + +在介绍ObString的接口之前,先介绍一下ObSring的内存管理方式,这样会更加容易理解ObString的接口设计。 + +与STL string最大的区别有两个: +1. ObString 不管理内存,内存由外部传入,内存buffer的生命周期也由外部控制; +2. ObString 并不以 '\0' 结尾。 + +这也是使用 ObString 时需要重点关注的点。 + +ObString 的内存由外部传入,内部保存三个成员变量: +```cpp + char *ptr_; /// 内存指针 + obstr_size_t buffer_size_; /// 内存buffer长度 + obstr_size_t data_length_; /// 有效数据长度 +``` + +> ObString 中都使用 obstr_size_t 表示长度,其类型是 int32_t + +参考 ObString 当前内存维护模式与字符串常用的接口,ObString 常用的接口如下: + +```cpp +/** + * 构造函数 + * + * 构造字符串的buffer数据和有效数据长度 + * + * 还有一些衍生的构造函数,比如省略buffer长度(buffer长度与数据长度一致) + */ +ObString(const obstr_size_t size, const obstr_size_t length, char *ptr); + +/** + * 是否空字符串 + */ +bool empty() const; + +/** + * 重新赋值一个新的buffer/字符串 + */ +void assign_buffer(char *buffer, const obstr_size_t size); + +/** + * 有效数据的长度,或者称为字符串的长度 + */ +obstr_size_t length() const; + +/** + * 内存buffer的长度 + */ +obstr_size_t size() const; + +/** + * 获取指针 + */ +const char *ptr() const; + +/** + * 不区分大小写进行比较 + * + * @NOTE: 虽然ObString没有说明是以'\0'结尾,但是这里实现时使用strncasecmp,所以使用此函数时要留意 + */ +int case_compare(const ObString &obstr) const; +int case_compare(const char *str) const; + +/** + * 区分大小写比较 + * + * @NOTE: 与case_compare相比较来说,这里没有使用strncmp,而是使用memcmp来比较的buffer长度 + */ +int compare(const ObString &obstr) const; +int32_t compare(const char *str) const; +``` + +ObString 还有一些其它的接口,需要时浏览下 ob_string.h 代码即可。 + +# 数组 + +OceanBase的数组接口设计与STL vector类似,只是更加符合OceanBase的风格。比如接口会有一个int返回值表示执行成功或失败。OceanBase 提供了多个不同实现的数组,不过它们提供的接口是类似的。 + +常用的数组实现类都继承了同一个接口 `ObIArray`。我们先看一下接口定义,然后再分别介绍不同的数组实现之间的差别。 + +## ObIArray + +数组的接口类中并没有指定内存分配器。 + +```cpp +/** + * 默认空构造函数 + */ +ObIArray(); + +/** + * 直接接受指定数组 + * + * 接口类不会接管data相关的内存,内存处理需要看具体的实现类。 + */ +ObIArray(T *data, const int64_t count); + +/** + * 类似 vector::push_back,在最后添加一个元素 + * @return 成功返回OB_SUCCESS + */ +int push_back(const T &obj); + +/** + * 移除最后一个元素 + * @note 很可能不会调用析构函数,需要看具体的实现类 + */ +void pop_back(); + +/** + * 移除最后一个元素,并将最后一个元素复制到obj + * @return 成功返回OB_SUCCESS + */ +int pop_back(T &obj); + +/** + * 移除指定位置的元素 + */ +int remove(int64_t idx); + +/** + * 获取指定位置的元素 + * @return 成功返回OB_SUCCESS。如果指定位置不存在,会返回失败 + */ +int at(int64_t idx, T &obj); + +/** + * 重置数组。类似vector::clear + */ +void reset(); + +/** + * 重用数组。具体看实现 + */ +void reuse(); + +/** + * 销毁此数组,作用与调用析构函数相同 + */ +void destroy(); + +/** + * 预留指定大小的内存空间。不会做对象初始化 + */ +int reserve(int64_t capacity); + +/** + * 预留指定大小的内存空间,通常实现类会执行对象的构造函数 + */ +int prepare_allocate(int64_t capacity); + +/** + * 从另一个数组复制并销毁当前数据 + */ +int assign(const ObIArray &other); +``` + +## ObArray +ObArray 自己管理内存,在声明ObArray模板类时,需要指定分配器,或者使用默认分配器 `ModulePageAllocator`。由于OceanBase要求所有的动作都要判断返回值,因此ObArray的 `operator=` 等不带返回值的函数,不建议使用。 + +ObArray的很多行为表现与STL vector类似,每次内存扩展时表现也类似,会扩展两倍当前数据大小,但最多 `block_size_` 大小。一个 `block_size_` 默认值是 `OB_MALLOC_NORMAL_BLOCK_SIZE` (可以认为是8K)。 + +代码参考 ob_array.h。 + +## ObSEArray +与ObArray类似,扩展时也会按照两倍大小,不超过`block_size_`。 + +与ObArray不同的是,ObSEArray多了一个模板参数 `LOCAL_ARRAY_SIZE`,不需要额外的内存分配即可容纳一定量的元素。因此OBSEArray可能可以直接使用栈内存而不是堆内存: + +```cpp +char local_data_buf_[LOCAL_ARRAY_SIZE * sizeof(T)]; +``` +如果后续空间不足,需要扩充,那 `local_data_buf_` 将不再存放有效数据而是另外申请内存。所以要综合考虑,给出一个合理的`LOCAL_ARRAY_SIZE`才能让ObSEArray效率比较高。 + +参考代码 `ob_se_array.h`。 + +## ObFixedArray +顾名思义,就是一个固定大小的数组。一旦容量大小确定下来就不能再变了。代码参考 `ob_fixed_array.h`。 + +## ObVector +ObVector 不属于 ObIArray的子类,其表现和接口设计与ObIArray很类似,所以使用 ObIArray的子类即可。如果有兴趣,请阅读源码 `ob_vector.h` 和 它的实现文件 `ob_vector.ipp`。 + +# 链表 +链表不像数组,没有统一的接口。不过这里的接口设计与STL中也是很相近的。最常用的链表有两个,一个是 ObList,另一个是 ObDList。 + +## ObList + +ObList 是一个普通的循环双链表,代码参考`ob_list.h`。在构造时,需要传入内存分配器。常用的接口如下。 +```cpp +/** + * 声明 + * @param T 元素类型 + * @param Allocator 内存分配器 + */ +template +class ObList; + +/** + * 构造函数。必须传入内存分配器 + */ +ObList(Allocator &allocator); + +/** + * 在链表结尾插入指定元素 + */ +int push_back(const value_type &value); + +/** + * 在链表开头插入指定元素 + */ +int push_front(const value_type &value); + +/** + * 释放最后一个元素 + * @note 并没有执行元素的析构函数 + */ +int pop_back(); + +/** + * 两个pop_front函数都是把第一个元素删掉,区别是一个会复制对象一个不会 + */ +int pop_front(value_type &value); +int pop_front(); + +/** + * 在指定位置插入指定元素 + */ +int insert(iterator iter, const value_type &value); + +/** + * 删除指定位置的元素 + * @return 返回删除成功或失败 + */ +int erase(iterator iter); + +/** + * 删除第一个与value值相同的元素 + * @return 没有找到元素也会返回成功 + */ +int erase(const value_type &value); + +/** + * 获取第一个元素 + */ +T &get_first(); +const T &get_first() const; + +/** + * 获取最后一个元素 + */ +T &get_last(); + +/** + * 与STL类似,ObList支持iterator相关的接口 + */ +iterator begin(); +const_iterator begin(); +iterator end(); +const_iterator end() const; + +/** + * 删除所有的元素 + */ +void clear(); + +/** + * 判断是否空链表 + */ +bool empty() const; + +/** + * 元素个数 + */ +int64_t size() const; +``` + +## ObDList + +> 代码参考 `ob_dlist.h`。 + +ObDList 也是一个双链表,与ObList不同的是,它的元素内存布局与内存管理方式不一样。ObList 由用户传入对象,ObList 内部申请内存复制对象,构造链表节点的前后指针。而 ObDList 由用户直接传入包含前后节点指针的对象。由于ObDList的这个特性,会导致它与使用STL list的方法不同。 + +ObDList 不管理内存也完全不需要管理内存,它的模板参数没有内存分配器,只有一个 `DLinkNode`,`DLinkNode` 需要包含你需要的元素对象、前后节点指针和并实现一些通用的操作(有辅助实现基类),ObDList 的声明和一些接口如下: +```cpp +template +class ObDList; + +/// 把当前链表上的元素都移动到list上去 +int move(ObDList &list); + +/// 获取头节点(不是第一个元素) +DLinkNode *get_header(); +const DLinkNode *get_header() const; + +/// 获取最后一个元素 +DLinkNode *get_last(); + +/// 获取第一个元素 +const DLinkNode *get_first() const; +const DLinkNode *get_first_const() const; + +/// 在尾巴上添加一个节点 +bool add_last(DLinkNode *e); + +/// 在头上添加一个节点 +bool add_first(DLinkNode *e); + +/// 在指定位置添加节点 +bool add_before(const DLinkNode *pos, DLinkNode *e); + +/// 指定节点移动到最前面 +bool move_to_first(DLinkNode *e); +/// 指定节点移动到最后 +bool move_to_last(DLinkNode *e); + +/// 删除最后一个节点 +DLinkNode *remove_last(); +/// 删除最前面一个节点 +DLinkNode *remove_first(); + +/// 在链表开头插入另一个链表 +void push_range(ObDList &range); + +/// 从开头删除指定个数的元素,删除的元素放到了range中 +void pop_range(int32_t num, ObDList &range); + +/// 是否空链表 +bool is_empty() const +/// 元素个数 +int32_t get_size() const +/// 删除指定元素 +DLinkNode *remove(DLinkNode *e); +/// 清空链表 +void clear(); +``` + +OceanBase 提供了辅助 `DLinkNode` 实现 `ObDLinkNode` 和 `ObDLinkDerived`,只需要使用任一复制类即可轻松地使用 ObDList。 + +在介绍这两个辅助类之前,先简单看一下一个基础的辅助接口实现 `ObDLinkBase`,它是上面两个辅助类的基类。它包含了 ObDList 需要的前后节点指针与一些基本的节点操作,两个辅助类都是通过继承基类来实现,而且只是使用方法不同而已。 + +第一个辅助类 ObDLinkNode,声明如下: +```cpp +template +struct ObDLinkNode: public ObDLinkBase > +``` + +给定自己的真实链表元素类型即可,缺点是获取到链表元素时,需要使用 `ObDLinkNode::get_data` 来获取自己的对象,比如 +```cpp +class MyObj; +ObDList> alist; + +ObDLinkNode *anode = OB_NEW(ObDLinkNode, ...); +alist.add_last(anode); + +ObDLinkNode *nodep = alist.get_first(); +MyObj &myobj = nodep->get_data(); +// do something with myobj +``` + +第二个辅助类 ObDLinkDerived,比ObDLinkNode使用更简单一些,它的声明是这样的: +```cpp +template +struct ObDLinkDerived: public ObDLinkBase, T +``` + +注意看,它直接继承了模板类T本身,也就是不需要再像ObDLinkNode一样通过 get_data获取真实对象,直接可以使用T的方法,复制上面的示例: +```cpp +class MyObj; +ObDList> alist; + +ObDLinkDerived *anode = OB_NEW(ObDLinkDerived, ...); +alist.add_last(anode); + +ObDLinkDerived *nodep = alist.get_first(); +// MyObj &myobj = nodep->get_data(); // no need any more +// MyObj *myobj = nodep; // nodep is a pointer to MyObj too +// do something with myobj or directly with nodep +``` + +由于 ObDList 不管理节点内存,那么使用时就需要特别小心,注意管理好各个元素的生命周期,在执行清理动作之前,比如`clear`、`reset`,一定要把内存先释放掉。ObDList的接口声明非常清晰,只是与STL::list命名习惯不同,可以直接参考代码 `ob_dlist.h` 的接口声明使用即可,不再罗列。 + +# Map +Map 是一个常用的数据结构,它的插入和查询的效率都非常高。通常情况下,Map有两种实现方法,一种是平衡查找树,典型的是红黑树,常见的编译器使用这种方式实现,一种是散列表,STL中是unordered_map。 + +OceanBase中实现了非常多的Map,其中包括平衡查找树实现 ObRbTree 和适用于不同场景的hash map,比如 ObHashMap、ObLinkHashMap和ObLinearHashMap。 + +> OceanBase 实现了很多种hash map,但是推荐使用这里介绍的几个,除非你对其它实现理解特别清晰。 + +## ObHashMap +ObHashMap 的实现在 ob_hashmap.h 中,为了方便理解 ObHashMap 的实现,我会对照STL::unordered_map来介绍。 + +### ObHashMap 介绍 +在STL中,unordered_map的声明如下: +```cpp +template< + class Key, /// 键类型 + class T, /// 值类型 + class Hash = std::hash, /// 根据Key计算hash值 + class KeyEqual = std::equal_to, /// 判断Key是否相等 + class Allocator = std::allocator> /// 内存分配器 +> class unordered_map; +``` + +模板参数中 Key 是我们的键值,T 就是我们值的类型,Hash 是根据键值计算hash值的类或函数,KeyEqual 是判断两个键值是否相等的方法,Allocator 是一个分配器,分配的对象是键和值组成在一起的pair。 + +OceanBase 中的声明是类似的: + +```cpp +template , + class _equal = equal_to<_key_type>, + class _allocer = SimpleAllocer::AllocType>, + template class _bucket_array = NormalPointer, + class _bucket_allocer = oceanbase::common::ObMalloc, + int64_t EXTEND_RATIO = 1> +class ObHashMap; +``` + +其中 `_key_type`、`_value_type`、`_hashfunc`、`_equal`,与STL::unordered_map的声明参数含义是一样的。这里多了一些参数: + +- `_defendmode`: OceanBase 提供了有限条件的线程安全hashmap实现,可以使用默认值,当前先忽略,稍后会介绍; +- `_allocer`与`_bucket_allocer`:STL::unordered_map只需要一个分配器,而这里要求提供两个分配器。hashmap中,通常会有一个数组作为桶(bucket)数组,元素进行hash后,找到对应桶,然后将元素”挂载“在对应的桶上。`_bucket_allocer` 就是桶数组的分配器,而`_allocer` 是元素的分配器,也就是建值对的分配器; +- EXTEND_RATIO:如果EXTEND_RATIO是1,就不会进行扩展。 + +### ObHashMap 接口介绍 +```cpp +/** + * ObHashMap的构造函数并不做什么事情。必须调用 create 才会进行真正的初始化。 + * create 函数的参数主要是 桶的个数 (bucket_num)和内存分配器的参数。 + * 合理的给出桶的个数,可以让hashmap运行的更加高效又不至于浪费太多内存。 + * + * 通过下面几个接口可以看到,可以提供两个内存分配器,一个是bucket数组的分配器, + * 一个是元素节点的分配器 + */ +int create(int64_t bucket_num, + const ObMemAttr &bucket_attr, + const ObMemAttr &node_attr); +int create(int64_t bucket_num, const ObMemAttr &bucket_attr); +int create(int64_t bucket_num, + const lib::ObLabel &bucket_label, + const lib::ObLabel &node_label = ObModIds::OB_HASH_NODE, + uint64_t tenant_id = OB_SERVER_TENANT_ID, + uint64_t ctx_id = ObCtxIds::DEFAULT_CTX_ID); +int create(int64_t bucket_num, + _allocer *allocer, + const lib::ObLabel &bucket_label, + const lib::ObLabel &node_label = ObModIds::OB_HASH_NODE); +int create(int64_t bucket_num, + _allocer *allocer, + _bucket_allocer *bucket_allocer); + +/// 直接销毁当前对象 +int destroy(); + +/// 这两个函数都会删除所有的元素 +int clear(); +int reuse(); + +/** + * 获取指定键值的元素值 + * 虽然也提供了get函数,但是建议使用当前函数。 + * @param timeout_us:获取元素的超时时间。超时的实现原理后面会介绍 + * @return 找到了返回成功 + */ +int get_refactored(const _key_type &key, _value_type &value, const int64_t timeout_us = 0) const; + +/** + * 设置某个键值的值 + * @param key: 建 + * @param value: 值 + * @param flag:0表示已经存在不会覆盖,否则会覆盖掉原先的值 + * @param broadcast:是否唤醒等待获取当前键的线程 + * @param overwrite_key:没使用 + * @param callback:插入或更新成功后,可以使用callback对值做一些额外操作 + */ +template +int set_refactored(const _key_type &key, + const _value_type &value, + int flag = 0, + int broadcast = 0, + int overwrite_key = 0, + _callback *callback = nullptr); + +/** + * 遍历所有元素 + * @note + * 1. 不能在遍历的过程中做删除元素、插入等动作。 + * 因为遍历的过程中会加一些锁,而插入、删除等动作也会加锁,所以可能会产生锁冲突; + * 2. callback 动作尽量小,因为它是在锁范围内工作的 + */ +template +int foreach_refactored(_callback &callback) const; + +/** + * 删除指定键值。如果value指针不是空,会返回对应元素 + * @return 元素不存在会返回OB_HASH_NOT_EXIST + */ +int erase_refactored(const _key_type &key, _value_type *value = NULL); + +/** + * 不存在就插入,否则调用callback来更新 + */ +template +int set_or_update(const _key_type &key, const _value_type &value, + _callback &callback); + +/** + * 删除指定键值并满足特定条件的元素 + */ +template +int erase_if(const _key_type &key, _pred &pred, bool &is_erased, _value_type *value = NULL); + +/** + * 不需要复制元素,直接以callback的方式访问指定键值的元素 + * @note callback 是在写锁保护下执行 + */ +template +int atomic_refactored(const _key_type &key, _callback &callback); + +/** + * 不需要将元素值复制出来,直接拿到元素通过callback来访问 + * @note callback是在读锁保护下执行的 + */ +template +int read_atomic(const _key_type &key, _callback &callback); +``` + +### ObHashMap的实现 +熟悉STL unordered_map实现原理的同学肯定能猜到ObHashMap的实现原理。ObHashMap的实现也是一个线性表,作为桶数组,再利用拉链表的方法解决键hash冲突。但是这里还有一些细节,希望能够帮助大家了解它的实现,而更高效地利用ObHashMap。 + +ObHashMap 底层依赖 ObHashTable,代码参考 `ob_hashtable.h`,ObHashMap 是在ObHashTable上封装了一下 Key Value 的语义而已。 + +**有条件的线程安全** + +如果模板参数`_defendmode` 选择有效的锁模式,而 ObHashTable 的每个桶都有一个读写锁,那么ObHashTable就会提供有条件的线程安全。在访问桶上的元素时,都会加对应的锁,包括带有 `callback` 的接口也是,所以`callback`中的动作应该尽量轻量而且不应该再访问ObHashTable的其它元素防止死锁。 + +ObHashMap 在扩容时不是线程安全的。如果提供的模板参数 EXTEND_RATIO 不是1,在需要的时候就会扩容,并且这对用户是透明的。 + +ObHashMap `_defendmode` 的默认值就是一个有效的线程安全保护模式 `LatchReadWriteDefendMode`。 + +**_defendmode** + +_defendmode 定义了不同的桶加锁方式,在 `ob_hashutils.h` 中提供了6中模式: + +1. LatchReadWriteDefendMode +2. ReadWriteDefendMode +3. SpinReadWriteDefendMode +4. SpinMutexDefendMode +5. MultiWriteDefendMode +6. NoPthreadDefendMode + +其中前5种都能提供线程安全保护,只是使用的锁模式不同。在不同的业务场景、不同的线程读写并发,选择合理的模式,可以提高效率和稳定性。而第6种模式 `NoPthreadDefendMode`,则不提供任何保护。 + +**get超时等待** + +如果在获取某个元素时指定的元素不存在,可以设置一个等待时间。ObHashTable将会在对应的桶上插入一个 `fake` 元素,然后等待。在另一个线程插入对应元素时,会唤醒等待线程,不过需要插入元素线程明确指定需要唤醒,即 set_refactor 的 broatcast 值设置为非0。 + +## ObHashSet +与 ObHashMap类似,ObHashSet是基于ObHashTable封装了一个只有key没有value的实现,请参考代码ob_hashset.h,不再赘述。 + +## ObLinkHashMap +ObLinkHashMap 是一个读写性能兼顾、线程安全(包括扩容)的无锁hash map,使用拉链式方法解决 hash 冲突。 +下面列举一下这个类的特点: + +- 读写性能兼顾; +- 基于无锁方案实现线程安全; +- 引入retire station,节点会延迟释放,因此建议 Key 尽量小; +- 存在一定的内存浪费; +- 扩缩容时采用批量搬迁方式完成; +- 有热点key时,get性能由于引用计数问题不佳; +- bucket 过多扩容时,初始化Array较慢。 + +> 关于 retire station,请参考论文 [Reclaiming Memory for Lock-Free Data Structures:There has to be a Better Way](https://www.cs.utoronto.ca/%7Etabrown/debra/fullpaper.pdf)。 + +下面列一些常用的接口以及使用时的注意事项。 + +```cpp +/** + * ObLinkHashMap的声明 + * 模板参数: + * @param Key 键值类型 + * @param Value 值的类型,需要继承自 LinkHashValue(参考 ob_link_hashmap_deps.h) + * @param AllocHandle 分配释放值和节点的类 (参考 ob_link_hashmap_deps.h) + * @param RefHandle 引用计数的函数。如果你没有深入理解它的原理,不要修改 + * @param SHRINK_THRESHOLD 当前节点的个数太多或者太少时就会扩缩容,尽量让当前节点保持在 + * 比例[1/SHRINK_THRESHOLD, 1]之间(非精准控制) + */ +template, + typename RefHandle=RefHandle, + int64_t SHRINK_THRESHOLD = 8> +class ObLinkHashMap; + + +/// 当前元素的个数 +int64_t size() const; + +/** + * 插入一个元素 + * @note 如果返回成功,需要执行 hash.revert(value) + */ +int insert_and_get(const Key &key, Value* value); + +/// 删除指定元素 +int del(const Key &key); + +/** + * 获取指定元素 + * @note 如果返回成功,需要执行 revert + */ +int get(const Key &key, Value*& value); + +/// 释放指定元素的引入计数。可以跨线程释放 +void revert(Value* value); + +/** + * 判断是否存在指定元素 + * @return OB_ENTRY_EXIST 存在 + */ +int contains_key(const Key &key); + +/** + * 遍历所有元素 + * @param fn : bool fn(Key &key, Value *value); 其中bool返回值表示是否还要继续遍历 + */ +template int for_each(Function &fn); + +/** + * 删除满足条件的元素 + * @param fn bool fn(Key &key, Value *value); 其中bool返回值表示是否需要删除 + */ +template int remove_if(Function &fn); +``` + +## ObRbTree +ObRbTree 是一个红黑树实现,支持插入、删除、查找等基本操作,非线程安全。由于ObRbTree 在OceanBase中并没有使用,因此不再介绍,有兴趣的请阅读源码 `ob_rbtree.h`。 + + +# 其它 +OceanBase 还有很多基础容器的实现,比如一些队列(ObFixedQueue、ObLightyQueue、ObLinkQueue)、bitmap(ObBitmap)、tuple(ObTuple)等。如果常见的容器不能满足你的需求,可以在 `deps/oblib/src/lib` 目录下找到更多。 diff --git a/docs/docs/zh/contributing.md b/docs/docs/zh/contributing.md new file mode 100644 index 000000000..a30f55e90 --- /dev/null +++ b/docs/docs/zh/contributing.md @@ -0,0 +1,99 @@ +# OceanBase 贡献指南 + +OceanBase 社区热情欢迎每一位对数据库技术热爱的开发者,期待携手开启思维碰撞之旅。无论是文档格式调整或文字修正、问题修复还是增加新功能,都是对 OceanBase 社区参与和贡献方式之一,立刻开启您的 First Contribution 吧! + +## 如何找到一个合适issue + +* 通过[good first issue](https://github.com/oceanbase/oceanbase/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)标签可以找到适合新手入门的issue +* 通过`bug`/`new feature`找到当前版本的bug和建议添加的功能 + 找到合适的issue之后,可以在issue下回复`/assign` 将issue分配给自己 + +## 代码贡献流程 + +以 Centos7 操作系统为例 + +### 1. Fork 项目仓库 + +1. 访问项目的 [GitHub 地址](https://github.com/oceanbase/oceanbase)。 +2. 点击 Fork 按钮创建远程分支。 + +### 2. 配置本地环境变量 + +```bash +working_dir=$HOME/workspace # 定义工作目录 +user={GitHub账户名} # 和github上的用户名保持一致 +``` + +### 3. 克隆代码 + +```bash +mkdir -p $working_dir +cd $working_dir +git clone git@github.com:$user/oceanbase.git +# 也可以使用: git clone https://github.com/$user/oceanbase + +# 添加上游分支 +cd $working_dir/oceanbase +git remote add upstream git@github.com:oceanbase/oceanbase.git +# 或: git remote add upstream https://github.com/oceanbase/oceanbase + +# 为上游分支设置 no_push +git remote set-url --push upstream no_push + +# 确认远程分支有效 +git remote -v +``` + +### 4. 创建新分支 + +```bash +# 更新本地 master 分支。 +new_branch_name={issue_xxx} # 设定分支名,建议直接使用issue+id的命名 +cd $working_dir/oceanbase +git fetch upstream +git checkout master +git rebase upstream/master +git checkout -b $new_branch_name +``` + +### 5. 开发 + +在新建的分支上完成开发 + +### 6. 提交代码 + +``` +# 检查本地文件状态 +git status + +# 添加您希望提交的文件 +# 如果您希望提交所有更改,直接使用 `git add .` +git add ... +# 为了让 github 自动将 pull request 关联上 github issue, +# 建议 commit message 中带上 "fixed #{issueid}", 其中{issueid} 为issue 的id, +git commit -m "fixed #xxxx: update the xx" + +# 在开发分支执行以下操作 +git fetch upstream +git rebase upstream/master +git push -u origin $new_branch_name +``` + +### 7. 创建 PR + +1. 访问您 Fork 的仓库。 +2. 单击 {new_branch_name} 分支旁的 Compare & pull request 按钮。 + +### 8. 签署 CLA 协议 + +签署[Contributor License Agreement (CLA)](https://cla-assistant.io/oceanbase/oceanbase) ;在提交 Pull Request 的过程中需要签署后才能进入下一步流程。如果没有签署,在提交流程会有如下报错: + +![image](https://user-images.githubusercontent.com/5435903/204097095-6a19d2d1-ee0c-4fb6-be2d-77f7577d75d2.png) + +### 9. 代码审查与合并 + +有review、合并权限的维护者,会帮助开发者进行代码review;review意见通过后,后续的操作都会由维护者进行,包括运行各项测试(目前包括centos和ubuntu的编译),最终代码会由维护者通过后合入 + +### 10. 祝贺成为贡献者 + +当 pull request 合并后, 则所有的 contributing 工作全部完成, 恭喜您, 您成为 OceanBase 贡献者. diff --git a/docs/docs/zh/debug.md b/docs/docs/zh/debug.md new file mode 100644 index 000000000..6b3daf99b --- /dev/null +++ b/docs/docs/zh/debug.md @@ -0,0 +1,423 @@ +--- +title: 调试 +--- + +# 背景 + +这里介绍一些调试 OceanBase 的方法。我们有很多种调试方法,比如 gdb,日志等。 + +建议编译 OceanBase 时使用 debug 模式,这样更容易调试。 + +# GDB +GDB 是一个强大的调试工具,但是使用 gdb 调试 OceanBase 是比较困难的,而且场景比较有限。 + +如果要调试单进程并且只有某一个线程,可以使用 gdb,否则建议使用日志。 + +假设已经部署了源码编译的 oceanbase。 + +调试 OceanBase 与调试其他 C++ 程序类似,你可以使用 gdb,如下: + +1. 找到进程 id +```bash +ps -ef | grep observer +``` + +或者 +```bash +pidof observer +``` + +2. attach 进程 +```bash +gdb observer +``` + +接着就可以设置断点,打印变量等。更多信息请参考 [gdb 手册](https://sourceware.org/gdb/current/onlinedocs/gdb.html/)。 + +## 使用 debug-info 包调试 OceanBase + +要调试RPM部署的OceanBase,或者查看 coredump 文件,需要先安装或者加载 debug-info 包。推荐使用加载的模式,因为系统中会有很多 debug-info 包,而且很难清理。 + +首先,从网上下载 debug-info 包,然后加载到gdb。之后,你就可以很容易地调试 OceanBase 了。 + +下面是一些提示。 + +**如何查找 debug-info 包** + +使用下面的命令获取包的revision。 +```bash +# in the observer runtime path +clusters/local/bin [83] $ ./observer -V +./observer -V +observer (OceanBase_CE 4.1.0.1) + +REVISION: 102000042023061314-43bca414d5065272a730c92a645c3e25768c1d05 +BUILD_BRANCH: HEAD +BUILD_TIME: Jun 13 2023 14:26:23 +BUILD_FLAGS: RelWithDebInfo +BUILD_INFO: + +Copyright (c) 2011-2022 OceanBase Inc. +``` + +如果看到下面的错误信息 +``` +./observer -V +./observer: error while loading shared libraries: libmariadb.so.3: cannot open shared object file: No such file or directory +``` + +就换成这个命令来获取revision +```bash +clusters/local/bin [83] $ LD_LIBRARY_PATH=../lib:$LD_LIBRARY_PATH ./observer -V +./observer -V +observer (OceanBase_CE 4.1.0.1) + +REVISION: 102000042023061314-43bca414d5065272a730c92a645c3e25768c1d05 +BUILD_BRANCH: HEAD +BUILD_TIME: Jun 13 2023 14:26:23 +BUILD_FLAGS: RelWithDebInfo +BUILD_INFO: + +Copyright (c) 2011-2022 OceanBase Inc. +``` + +**下载 debug-info 包** + +上面输出的版本信息中,我们需要的是 revision 字段的前半部分,即 +``` +REVISION: 102000042023061314-43bca414d5065272a730c92a645c3e25768c1d05 +``` +这个是我们需要的:`102000042023061314`。 + +接着在RPM网站上搜索 `102000042023061314`。 + +![download debug info package](images/download-debug-info-package.png) + +RPM网站列表: +- [x86_64 for el7](http://mirrors.aliyun.com/oceanbase/community/stable/el/7/x86_64/) +- [x86_64 for el8](https://mirrors.aliyun.com/oceanbase/community/stable/el/8/x86_64/) +- [aarch64(arm) for el7](https://mirrors.aliyun.com/oceanbase/community/stable/el/7/aarch64/) +- [aarch64(arm) for el8](https://mirrors.aliyun.com/oceanbase/community/stable/el/8/aarch64/) + +**从RPM中提取 debug-info package** + +从RPM中提取 debug-info 包,例如 +```bash +rpm2cpio oceanbase-ce-debuginfo-4.1.0.1-102000042023061314.el7.x86_64.rpm | cpio -div +``` + +解开后是这样的 + +```bash +~/tmp/debug-info [83] $ tree -a +. +└── usr + └── lib + └── debug + ├── .build-id + │ └── ee + │ ├── f87ee72d228069aab083d8e6d2fa2fcb5c03f2 -> ../../../../../home/admin/oceanbase/bin/observer + │ └── f87ee72d228069aab083d8e6d2fa2fcb5c03f2.debug -> ../../home/admin/oceanbase/bin/observer.debug + └── home + └── admin + └── oceanbase + └── bin + └── observer.debug +``` + +`observer.debug` 是我们要的 debug-info 包,`f87ee72d228069aab083d8e6d2fa2fcb5c03f2.debug` 是一个软链接。 + +**使用 debug-info 包调试 OceanBase** + +使用gdb命令 attch 到一个进程或者打开coredump文件。 + +```bash +# attach 进程 +gdb ./observer `pidof observer` +``` + +或 + +```bash +# 打开coredump文件 +gdb ./observer +``` + +正常情况下,会看到这个信息 + +``` +Type "apropos word" to search for commands related to "word"... +Reading symbols from clusters/local/bin/observer... +(No debugging symbols found in clusters/local/bin/observer) +Attaching to program: clusters/local/bin/observer, process 57296 +``` + +意思是没有调试符号。 + +运行一些调试命令,比如 `bt`,会看到这个信息。 + +```bash +(gdb) bt +#0 0x00007fb6e9c36d62 in pthread_cond_timedwait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0 +#1 0x00007fb6f9f44862 in ob_pthread_cond_timedwait () +#2 0x00007fb6eee8d206 in oceanbase::common::ObThreadCond::wait_us(unsigned long) () +#3 0x00007fb6f34b21c8 in oceanbase::observer::ObUniqTaskQueue::run1() () +#4 0x00007fb6f9f44259 in oceanbase::lib::Threads::run(long) () +#5 0x00007fb6f9f40aca in oceanbase::lib::Thread::__th_start(void*) () +``` + +看不到源码文件名和参数信息。 + +现在加载 debug-info 包。 + +```bash +(gdb) symbol-file usr/lib/debug/home/admin/oceanbase/bin/observer.debug +Reading symbols from usr/lib/debug/home/admin/oceanbase/bin/observer.debug... +``` + +> 使用debug-info文件的全路径更好 + +再次执行调试命令,就可以看到详细信息。 + +```bash +(gdb) bt +#0 0x00007fb6e9c36d62 in pthread_cond_timedwait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0 +#1 0x00007fb6f9f44862 in ob_pthread_cond_timedwait (__cond=0x7fb6fb1d5340, __mutex=0x7fb6fb1d5318, __abstime=0x7fb6b3ed41d0) + at deps/oblib/src/lib/thread/ob_tenant_hook.cpp:124 +#2 0x00007fb6eee8d206 in oceanbase::common::ObThreadCond::wait_us (this=, time_us=140422679606016) + at deps/oblib/src/lib/lock/ob_thread_cond.cpp:106 +#3 0x00007fb6f34b21c8 in oceanbase::common::ObThreadCond::wait (this=0x7fb6fb1d5310, time_ms=200) + at deps/oblib/src/lib/lock/ob_thread_cond.h:69 +#4 oceanbase::observer::ObUniqTaskQueue::run1 ( + this=) at src/observer/ob_uniq_task_queue.h:417 +``` + +# 日志 + +日志是调试 OceanBase 最常用的方法,易于使用,适用于大多数场景。 +在常见的场景中,可以在代码中添加日志并打印变量,然后重新编译和部署 oceanbase。 + +## 如何加日志 + +可以在源码中找到日志代码,比如 +```cpp +LOG_DEBUG("insert sql generated", K(insert_sql)); +``` + +`LOG_DEBUG` 是打印DEBUG级别的日志宏。 + +这跟其它的程序有点不太一样。第一个参数是一个字符串,其他的参数通常是 `K(variable_name)`。`K` 是一个宏,用来打印变量名和值。 + +## 如何搜索日志 + +日志文件在 OceanBase 运行目录的 `log` 目录下。你可以使用 `grep` 命令搜索日志。 + +一个日志的例子。 +``` +[2023-07-05 16:40:42.635136] INFO [SQL.EXE] explicit_start_trans (ob_sql_trans_control.cpp:194) [88022][T1003_ArbSer][T1003][YD9F97F000001-0005FFB71FCF95C7-0-0] [lt=42] start_trans(ret=0, tx_id={txid:2118151}, session={this:0x7ff2663d6188, id:1, tenant:"sys", tenant_id:1, effective_tenant:"sys", effective_tenant_id:1003, database:"oceanbase", user:"root@%", consistency_level:3, session_state:0, autocommit:true, tx:0x7ff26b0e4300}, read_only=false, ctx.get_execution_id()=18446744073709551615) +``` + +时间戳(`[2023-07-05 16:40:42.635136]`), 日志级别(`INFO`), 模块名称(`[SQL.EXE]`), 函数名称(`explicit_start_trans`), 代码文件(`ob_sql_trans_control.cpp`), 行号(`194`), 线程号(`88022`), 线程名称(`T1003_ArbSer`), trace id(`YD9F97F000001-0005FFB71FCF95C7-0-0`), 等等. + +每个SQL请求都有一个唯一的 `trace id`。可以通过 `trace id` 来找到所有与指定SQL请求相关的日志。 + +## 日志的一些技巧 +**Trace ID** + +使用下面的SQL命令可以获取上一次执行SQL请求的 trace id。 +```sql +select last_trace_id(); +``` +**日志级别** + +使用下面的命令调整日志级别。 +```sql +set ob_log_level=debug; +``` +**Log 流量控制** + +如果找不到想要的日志,可能是被限流了,可以使用下面的命令调整日志流量控制。 + +```sql +alter system set syslog_io_bandwidth_limit='1G'; +alter system set diag_syslog_per_error_limit=1000; +``` +**同步打印日志** + +用下面的命令可以同步打印日志。 +```sql +alter system set enable_async_syslog='False'; +``` +**打印调用栈** + +在日志中可以这样打印调用栈 +```cpp +LOG_DEBUG("insert sql generated", K(insert_sql), K(lbt())); +``` +假设看到这样的信息: +```txt +lbt()="0x14371609 0xe4ce783 0x54fd9b6 0x54ebb1b 0x905e62e 0x92a4dc8 0x905df11 0x905dc94 0x13d2278e 0x13d22be3 0x6b10b81 0x6b0f0f7 0x62e2491 0x10ff6409 0x1475f87a 0x10ff6428 0x1475f1c2 0x1476ba83 0x14767fb5 0x14767ae8 0x7ff340250e25 0x7ff33fd0ff1d" +``` + +用这个命令查看调用栈信息: +```bash +addr2line -pCfe ./bin/observer 0x14371609 0xe4ce783 0x54fd9b6 0x54ebb1b 0x905e62e 0x92a4dc8 0x905df11 0x905dc94 0x13d2278e 0x13d22be3 0x6b10b81 0x6b0f0f7 0x62e2491 0x10ff6409 0x1475f87a 0x10ff6428 0x1475f1c2 0x1476ba83 0x14767fb5 0x14767ae8 0x7ff340250e25 0x7ff33fd0ff1d +``` + +我测试时看到的是这样的 + +```txt +oceanbase::common::lbt() at /home/distcc/tmp/./deps/oblib/src/lib/utility/ob_backtrace.cpp:130 (discriminator 2) +operator() at /home/distcc/tmp/./src/sql/session/ob_basic_session_info.cpp:599 (discriminator 2) +oceanbase::sql::ObBasicSessionInfo::switch_tenant(unsigned long) at /home/distcc/tmp/./src/sql/session/ob_basic_session_info.cpp:604 +oceanbase::observer::ObInnerSQLConnection::switch_tenant(unsigned long) at /home/distcc/tmp/./src/observer/ob_inner_sql_connection.cpp:1813 (discriminator 2) +... +oceanbase::lib::Thread::run() at /home/distcc/tmp/./deps/oblib/src/lib/thread/thread.cpp:162 +oceanbase::lib::Thread::__th_start(void*) at /home/distcc/tmp/./deps/oblib/src/lib/thread/thread.cpp:312 +?? ??:0 +?? ??:0 +``` + +# SQL + +一些SQL命令也可以帮助调试,首先运行下面的命令开启trace功能。 +```sql +-- on 4.x +set ob_enable_show_trace=1; +``` + +然后运行SQL命令,比如: +```sql +select * from t, t1 where t.id=t1.id; +``` + +之后,你可以运行下面的命令获取trace信息。 +```sql +show trace; +``` + +假设看到这样的信息 +```txt +obclient> show trace; ++-------------------------------------------+----------------------------+------------+ +| Operation | StartTime | ElapseTime | ++-------------------------------------------+----------------------------+------------+ +| com_query_process | 2023-07-06 15:30:49.907532 | 9.547 ms | +| └── mpquery_single_stmt | 2023-07-06 15:30:49.907552 | 9.506 ms | +| ├── sql_compile | 2023-07-06 15:30:49.907615 | 6.605 ms | +| │ ├── pc_get_plan | 2023-07-06 15:30:49.907658 | 0.024 ms | +| │ └── hard_parse | 2023-07-06 15:30:49.907763 | 6.421 ms | +| │ ├── parse | 2023-07-06 15:30:49.907773 | 0.119 ms | +| │ ├── resolve | 2023-07-06 15:30:49.907952 | 0.780 ms | +| │ ├── rewrite | 2023-07-06 15:30:49.908857 | 1.320 ms | +| │ ├── optimize | 2023-07-06 15:30:49.910209 | 3.002 ms | +| │ ├── code_generate | 2023-07-06 15:30:49.913243 | 0.459 ms | +| │ └── pc_add_plan | 2023-07-06 15:30:49.914016 | 0.140 ms | +| └── sql_execute | 2023-07-06 15:30:49.914239 | 2.675 ms | +| ├── open | 2023-07-06 15:30:49.914246 | 0.217 ms | +| ├── response_result | 2023-07-06 15:30:49.914496 | 1.956 ms | +| │ └── do_local_das_task | 2023-07-06 15:30:49.914584 | 0.862 ms | +| └── close | 2023-07-06 15:30:49.916474 | 0.415 ms | +| ├── close_das_task | 2023-07-06 15:30:49.916486 | 0.037 ms | +| └── end_transaction | 2023-07-06 15:30:49.916796 | 0.064 ms | ++-------------------------------------------+----------------------------+------------+ +18 rows in set (0.01 sec) +``` + +# Debug Sync + +在使用 gdb 调试 OceanBase 的时候,可能会出现问题,因为 gdb 会挂起进程,而 OceanBase 依赖心跳来正常工作。所以我们提供了一个 debug sync 机制来解决这个问题。 + +在代码中增加一个 debug sync 点,特定的线程会在这个点挂起,然后你可以做一些事情来调试这个进程,比如使用 gdb attach 进程,或者执行一些 SQL 命令来获取一些信息。 + +> Debug Sync 在 release 模式下也可以用,所以它在生产环境中是开启的。 + +## 如何使用 + +**在代码中增加一个debug sync** + +打开文件 `ob_debug_sync_point.h`,在宏 `OB_DEBUG_SYNC_POINT_DEF` 中增加 debug sync 定义。比如: + +```cpp +#define OB_DEBUG_SYNC_POINT_DEF(ACT) \ + ACT(INVALID_DEBUG_SYNC_POINT, = 0) \ + ACT(NOW,) \ + ACT(MAJOR_FREEZE_BEFORE_SYS_COORDINATE_COMMIT,) \ + ACT(BEFORE_REBALANCE_TASK_EXECUTE,) \ + ACT(REBALANCE_TASK_MGR_BEFORE_EXECUTE_OVER,) \ + ACT(UNIT_BALANCE_BEFORE_PARTITION_BALANCE,) +``` + +要调试哪个函数,就在对应的函数加上 debug sync。比如: + +```cpp +int ObRootService::do_restart() +{ + int ret = OB_SUCCESS; + + const int64_t tenant_id = OB_SYS_TENANT_ID; + SpinWLockGuard rs_list_guard(broadcast_rs_list_lock_); + ... + DEBUG_SYNC(BEFORE_UNIT_MANAGER_LOAD); + ... +} +``` + +可以在任何地方加 debug sync。 + +**开启 Debug Sync** + +默认情况下,debug sync 是关闭的,通过下面的 SQL 命令开启它。 + +```sql +alter system set debug_sync_timeout='100000s'; +``` + +将 `debug_sync_timeout` 设置为大于0的数字就可以开启。 + +> 注意:`debug_sync_timeout` 的单位是微秒。 + +**开启你自己的 debug sync point** + +用这个命令开启自己加上的 debug sync。 +```sql +set ob_global_debug_sync = 'BEFORE_UNIT_MANAGER_LOAD wait_for signal_name execute 10000'; +``` + +`execute` 的意思是 debug sync 动作会在执行 10000 次后被禁用。 + +`signal_name` 是唤醒的名字。 + +当某个线程执行到debug sync时,就会停下来,这时候就可以执行一些调试操作。 + +**唤醒 debug sync point** + +用下面的命令唤醒 debug sync。 +```sql +set ob_global_debug_sync = 'now signal signal_name'; +-- or +set ob_global_debug_sync = 'now broadcast signal_name'; +``` +`signal_name` 是你在开启 debug sync 点时设置的名字。 + +然后hang的线程会继续执行。 + +**清理 debug sync point** + +调试完成后,需要清理掉 debug sync 点,可以使用下面的命令清理掉。 +```sql +set ob_global_debug_sync = 'BEFORE_UNIT_MANAGER_LOAD clear'; +``` + +**关闭 debug sync** + +使用下面的命令关闭 debug sync。 +```sql +alter system set debug_sync_timeout=0; +``` + +## debug sync 工作原理 + +当运行到指定的 debug sync 时,进程会使用 `condition_variable` 来等待信号,然后会挂起在 debug sync 点。当收到信号后,进程会继续执行。 + +可以查看代码 `ob_debug_sync.cpp/.h` 来了解更多关于 debug sync 机制的信息。 diff --git a/docs/docs/zh/ide-settings.md b/docs/docs/zh/ide-settings.md new file mode 100644 index 000000000..aceabe5f9 --- /dev/null +++ b/docs/docs/zh/ide-settings.md @@ -0,0 +1,162 @@ +--- +title: IDE 配置 +--- + +# 背景 + +为了更好的阅读OceanBase的代码,我们建议使用一个可以方便的索引OceanBase代码的IDE。在Windows下,我们推荐使用`Source Insight`,在Mac或者Linux下,我们推荐使用`VSCode + ccls`来阅读OceanBase的代码。由于`Source Insight`使用起来非常简单,所以本文档不介绍如何使用`Source Insight`。 + +本篇文档介绍如何设置 `VSCode + ccls`,这个组合非常适合阅读OceanBase的代码。[ccls](https://github.com/MaskRay/ccls) 是基于 [cquery](https://github.com/jacobdufault/cquery) 的,cquery是一个C/C++/Objective-C的 [LSP](https://en.wikipedia.org/wiki/Language_Server_Protocol) 之一(简单来说,LSP用于提供编程语言特定的功能,如代码补全、语法高亮、警告和错误标记,以及重构例程)。 + +OceanBase 的代码量非常大,而且OceanBase不能在Mac或者Windows下编译,所以我们建议在远程服务器上下载代码,然后在本地使用VSCode访问远程服务器上的代码。 + +# 在远程服务器上配置 ccls + +**注意** + +下面的 `/path/to` 只是一个路径示例,请替换成你的实际路径。 + +## 介绍 + +在 C/C++ LSP 领域,比较有名的工具有 clangd 和 ccls。这里我们推荐 ccls,因为: + +1. ccls 构建索引的速度比 clangd 慢,但是构建完成后,ccls 访问索引的速度比 clangd 快。 +2. clangd 不支持 unity 编译,而 OceanBase 是通过 unity 编译的,clangd 无法通过 compile_commands.json 构建索引。 + +# ccls 安装 + +## 在 CentOS 上安装 ccls + +> 注意:如果没有权限执行 `yum`,请使用 `sudo yum ...`。 + +```bash +yum install epel-release +yum install snapd # On centos8: yum install snapd --nobest +systemctl enable --now snapd.socket +ln -s /var/lib/snapd/snap /snap +snap install ccls --classic +``` + +然后把下面的命令添加到你的环境变量文件中,例如 '~/.bashrc' 或者 '~/.bash_profile' + +```bash +export PATH=/var/lib/snapd/snap/bin:$PATH +``` + +刷新一下环境变量,例如: + +```bash +source ~/.bashrc # or +source ~/.bash_profile +``` + +## 在 Ubuntu 上安装 ccls + +```bash +apt-get -y install ccls +``` + +> 注意:如果没有权限执行 `apt-get`,请使用 `sudo apt-get ...`。 + +## 检查安装是否成功 + +运行这个命令判断是否安装成功。 + +```bash +ccls --version +``` + +# VSCode 配置 + +## 远程插件 + +在远程主机上下载源代码后,可以很容易地在远程主机上设置调试环境。同时,由于远程主机更强大,应用程序可以更快地运行。即使网络出现问题,用户也可以很容易地访问远程主机上的源代码,只需在重新连接远程服务器后等待重新加载即可。 + +### 安装 + +在VSCode的扩展商店中下载并安装Remote插件。 + +![remote plugin](images/ide-settings-remote-plugin.png) + +### 使用 + +**注意**:确保本地机器和远程机器之间的连接正常。 + +安装插件后,VSCode左下角会出现一个图标。 + +![remote plugin usage](images/ide-settings-remote-plugin-usage.png) + +点击图标,选择 `Connect to Host`,或者按快捷键 `ctrl+shift+p`,选择 `Remote-SSH:Connect to Host`: + +![connec to remote ](images/ide-settings-connect-to-remote-server.png) + +输入远程服务器的`用户名@IP地址`,然后输入密码,VSCode会连接到远程服务器,准备打开远程机器的文件或目录。 + +![input password](images/ide-settings-input-password.png) + +输入密码之后,VSCode会连接到远程服务器,准备打开远程机器的文件或目录。 + +如果需要指定端口,请选择 `Add New SSH Host`,然后输入ssh命令,然后选择一个配置文件来存储ssh配置。 + +![ssh port](images/ide-settings-use-different-ssh-port.png) + +![ssh config file](images/ide-settings-choose-ssh-config.png) + +接着,配置好的机器可以在 `Connect to Host` 中找到。 + +每次都需要输入密码,如果想跳过这个步骤,可以配置SSH免密登录。 + +## C/C++ 插件 + +不推荐使用C/C++插件,因为无法为OceanBase提供良好的索引功能,并且与ccls插件不兼容。 + +如果有一些简单的场景,可以在VSCode的扩展商店中下载并安装C/C++插件。 + +![cpp plugins](images/ide-settings-cpp-plugins.png) + +C/C++插件可以自动完成代码和语法高亮,但是这个插件无法为OceanBase构建索引,很难跳转到OceanBase的符号。 + +## ccls 插件 + +### 安装 ccls 插件 + +![ccls plugin](images/ide-settings-ccls-plugin.png) + +> 要使用 ccls,就建议卸载 C/C++ 插件。 + +### 配置 ccls 插件 + +1. 插件设置按钮然后选择 **Extension Settings** + +![ccls plugin settings](images/ide-settings-ccls-plugin-settings.png) + +2. 设置 `ccls.index.threads`。ccls 默认使用系统80%的CPU核心作为默认的并行度。我们可以在VSCode的配置页面中搜索 `threads` 并设置如下数字。 + +> 默认情况下,OceanBase使用unity编译,比普通情况下消耗更多的内存。如果并行度太高,例如8核16G的系统,系统可能会挂起。 + +![ccls threads config](images/ide-settings-ccls-threads-config.png) + +## 使用 + +1. 使用 git 下载源码 [https://github.com/oceanbase/oceanbase](https://github.com/oceanbase/oceanbase) +2. 生成 `compile_commands.json` + + ```bash + bash build.sh ccls --init + ``` + +可以在 OceanBase 源码下看到 `compile_commands.json` 文件。 + +执行完上面的步骤后,需要重启 VSCode,然后在 VSCode 底部可以看到构建索引的过程: + +![ccls-indexing](images/ide-settings-ccls-indexing.png) + +索引构建完成后,可以很容易地找到任何打开文件的函数引用和类成员,如下面的示例: + +![ccls index example](images/ide-settings-ccls-index-example.png) + +一些快捷键设置 + +![ccls shortkey](images/ide-settings-ccls-keyboard-settings.png) + +![ccls shortkey](images/ide-settings-ccls-keyboard-settings2.png) diff --git a/docs/docs/zh/images/architecture.jpg b/docs/docs/zh/images/architecture.jpg new file mode 100644 index 000000000..e4f25463b Binary files /dev/null and b/docs/docs/zh/images/architecture.jpg differ diff --git a/docs/docs/zh/images/download-debug-info-package.png b/docs/docs/zh/images/download-debug-info-package.png new file mode 100644 index 000000000..6b5c6d1c3 Binary files /dev/null and b/docs/docs/zh/images/download-debug-info-package.png differ diff --git a/docs/docs/zh/images/ide-settings-ccls-index-example.png b/docs/docs/zh/images/ide-settings-ccls-index-example.png new file mode 100644 index 000000000..3c4b510fe Binary files /dev/null and b/docs/docs/zh/images/ide-settings-ccls-index-example.png differ diff --git a/docs/docs/zh/images/ide-settings-ccls-indexing.png b/docs/docs/zh/images/ide-settings-ccls-indexing.png new file mode 100644 index 000000000..173b26559 Binary files /dev/null and b/docs/docs/zh/images/ide-settings-ccls-indexing.png differ diff --git a/docs/docs/zh/images/ide-settings-ccls-keyboard-settings.png b/docs/docs/zh/images/ide-settings-ccls-keyboard-settings.png new file mode 100644 index 000000000..fcbd5f9d1 Binary files /dev/null and b/docs/docs/zh/images/ide-settings-ccls-keyboard-settings.png differ diff --git a/docs/docs/zh/images/ide-settings-ccls-keyboard-settings2.png b/docs/docs/zh/images/ide-settings-ccls-keyboard-settings2.png new file mode 100644 index 000000000..2ddb177f8 Binary files /dev/null and b/docs/docs/zh/images/ide-settings-ccls-keyboard-settings2.png differ diff --git a/docs/docs/zh/images/ide-settings-ccls-plugin-settings.png b/docs/docs/zh/images/ide-settings-ccls-plugin-settings.png new file mode 100644 index 000000000..2e3462be6 Binary files /dev/null and b/docs/docs/zh/images/ide-settings-ccls-plugin-settings.png differ diff --git a/docs/docs/zh/images/ide-settings-ccls-plugin.png b/docs/docs/zh/images/ide-settings-ccls-plugin.png new file mode 100644 index 000000000..56b7e9e72 Binary files /dev/null and b/docs/docs/zh/images/ide-settings-ccls-plugin.png differ diff --git a/docs/docs/zh/images/ide-settings-ccls-threads-config.png b/docs/docs/zh/images/ide-settings-ccls-threads-config.png new file mode 100644 index 000000000..a1d4b871f Binary files /dev/null and b/docs/docs/zh/images/ide-settings-ccls-threads-config.png differ diff --git a/docs/docs/zh/images/ide-settings-choose-ssh-config.png b/docs/docs/zh/images/ide-settings-choose-ssh-config.png new file mode 100644 index 000000000..3fa3659f2 Binary files /dev/null and b/docs/docs/zh/images/ide-settings-choose-ssh-config.png differ diff --git a/docs/docs/zh/images/ide-settings-connect-to-remote-server.png b/docs/docs/zh/images/ide-settings-connect-to-remote-server.png new file mode 100644 index 000000000..c80f0c81a Binary files /dev/null and b/docs/docs/zh/images/ide-settings-connect-to-remote-server.png differ diff --git a/docs/docs/zh/images/ide-settings-cpp-plugins.png b/docs/docs/zh/images/ide-settings-cpp-plugins.png new file mode 100644 index 000000000..dff3acb78 Binary files /dev/null and b/docs/docs/zh/images/ide-settings-cpp-plugins.png differ diff --git a/docs/docs/zh/images/ide-settings-input-password.png b/docs/docs/zh/images/ide-settings-input-password.png new file mode 100644 index 000000000..4f9851738 Binary files /dev/null and b/docs/docs/zh/images/ide-settings-input-password.png differ diff --git a/docs/docs/zh/images/ide-settings-remote-plugin-usage.png b/docs/docs/zh/images/ide-settings-remote-plugin-usage.png new file mode 100644 index 000000000..a0961df3f Binary files /dev/null and b/docs/docs/zh/images/ide-settings-remote-plugin-usage.png differ diff --git a/docs/docs/zh/images/ide-settings-remote-plugin.png b/docs/docs/zh/images/ide-settings-remote-plugin.png new file mode 100644 index 000000000..e5de46d97 Binary files /dev/null and b/docs/docs/zh/images/ide-settings-remote-plugin.png differ diff --git a/docs/docs/zh/images/ide-settings-use-different-ssh-port.png b/docs/docs/zh/images/ide-settings-use-different-ssh-port.png new file mode 100644 index 000000000..90ae2522d Binary files /dev/null and b/docs/docs/zh/images/ide-settings-use-different-ssh-port.png differ diff --git a/docs/docs/zh/images/unittest-ci-details.png b/docs/docs/zh/images/unittest-ci-details.png new file mode 100644 index 000000000..b9f400e88 Binary files /dev/null and b/docs/docs/zh/images/unittest-ci-details.png differ diff --git a/docs/docs/zh/images/unittest-github-ci.png b/docs/docs/zh/images/unittest-github-ci.png new file mode 100644 index 000000000..ca3614328 Binary files /dev/null and b/docs/docs/zh/images/unittest-github-ci.png differ diff --git a/docs/docs/zh/images/unittest-unittest.png b/docs/docs/zh/images/unittest-unittest.png new file mode 100644 index 000000000..efb4e7c83 Binary files /dev/null and b/docs/docs/zh/images/unittest-unittest.png differ diff --git a/docs/docs/zh/logging.md b/docs/docs/zh/logging.md new file mode 100644 index 000000000..7d4fcf1a1 --- /dev/null +++ b/docs/docs/zh/logging.md @@ -0,0 +1,382 @@ +--- +title: OceanBase 系统日志介绍 +--- + +# OceanBase 系统日志介绍 + +## Introduction + +本篇文档主要介绍OceanBase的系统日志,包括日志的分类、级别,如何在程序中输出日志,以及一些日志的实现细节。 + + +## System Log Introduction + +与常见的应用系统类似,系统日志是OceanBase开发人员排查问题时常用的重要手段之一。 + +OceanBase 的系统日志存储在observer安装路径下的log目录下面。系统日志主要分为两类: + +1. 普通日志:log 后缀,打印某一模块的所有日志(包含警告日志)。 +1. Ordinary logs: with log suffix, printed all logs (including warning logs) of a certain module. + +2. 警告日志:log.wf 后缀,只打印某一模块的WARN级别及以上的日志。 +2. Warning log: with log.wf suffix, only printed the warn level of a module and above. + +| 日志文件名称 log file name | 记录的信息 record information | +| ------------------ | ----------------------------- | +| observer.log[.wf] | 一般日志(警告日志、一般查询日志、其他日志) General logs (warning logs, general query logs, other logs) | +| rootservice.log[.wf] | rootservice模块的日志(包含全局ddl日志)rootService module log (including global DDL log) | +| election.log[.wf] | 选举相关日志 Election related logs | +| trace.log | 全链路追踪日志 Full link tracking log | + +> 比较特殊的是,trace.log 没有对应的".wf"日志。 +> wf 日志除了输出普通的日志,还有一条特殊的INFO日志,就是每次日志文件创建时会记录一些当前系统、进程的信息。 + +### 日志参数 Log Parameters + +目前syslog相关的有7个参数,都是动态生效,即可以在运行时动态调整。 + +| 配置项 | 数据类型 | 值域 | 默认值 | 描述 | +| --------------------------- | ---- | ----------------------------------------- | ------ | ------------------------------------- | +| enable_syslog_recycle | 布尔 | | False | 是否回收重启之前的旧日志(时间戳最小的日志文件) | +| enable_syslog_wf | 布尔 | | True | 是否打印WARN级别及以上的日志到单独的wf文件中 | +| enable_async_syslog | 布尔 | | True | 是否异步打印日志 | +| max_syslog_file_count | 整型 | \[0, +∞) | 0 | 每种只读日志文件最大保留数量 | +| syslog_io_bandwidth_limit | 字符串 | 0,其他合法大小 | "30MB" | 日志IO带宽限制 | +| syslog_level | 字符串 | DEBUG, TRACE, WDIAG, EDIAG, INFO, WARN, ERROR | WDIAG | 日志打印最低等级,该等级及以上的日志都打印 | +| diag_syslog_per_error_limit | 整型 | \[0, +∞) | 200 | 每个错误码每秒允许的 DIAG 系统日志数,超过该阈值后,日志将不再打印。 | + +> 这里所有的参数都是集群级的,并且都是动态生效。 +> 参数定义可以参考 ob_parameter_seed.ipp 文件。 + +## 日志回收 + +OceanBase的日志可以配置文件个数上限,以防止日志文件占用过大的磁盘空间。 + +如果 `enable_syslog_recycle = true` 且 `max_syslog_file_count > 0` ,每种日志文件的数量不能超过 `max_syslog_file_count`。日志内容刷新到磁盘中时触发旧日志文件回收。 + +每次执行 `flush_logs_to_file` 函数写日志时,如果某种文件写入了数据,就要检查是否需要将当前日志文件进行归档(有可能达到了大小上限)。 +在 `max_syslog_file_count > 0` 的前提下,就可能会调用 `ObLogger::rotate_log` 函数,如果该种日志文件数量超过上限 `max_syslog_file_count`,就会删掉最旧的一个日志文件。 + +新日志文件都会在开头打印一个特殊日志,信息包含当前节点的IP和端口、版本号、以及一些系统信息,参考 `ObLogger::log_new_file_info`。 + +``` +[2023-12-26 13:15:58.612579] INFO New syslog file info: [address: "127.0.0.1:2882", observer version: OceanBase_CE 4.2.1.1, revision: 101010012023111012-2f6924cd5a576f09d6e7f212fac83f1a15ff531a, sysname: Linux, os release: 3.10.0-327.ali2019.alios7.x86_64, machine: x86_64, tz GMT offset: 08:00] +``` + +## 日志级别 + +与常见的系统打日志方法类似,OceanBase 也提供了日志宏来打印不同级别的日志: + +| 级别 | 代码宏 | 说明 | +| ----- | --------- | ---- | +| DEBUG | LOG_DEBUG | 开发人员调试日志 | +| TRACE | LOG_TRACE | 事件跟踪日志,通常也是开发人员查看 | +| INFO | LOG_INFO | 系统状态变化日志 | +| WARN | LOG_DBA_WARN | 面向DBA的日志。出现非预期场景,observer能提供服务,但行为可能不符合预期,比如我们的写入限流 | +| ERROR | LOG_DBA_ERROR | 面向DBA的日志。observer不能提供正常服务的异常,如磁盘满监听端口被占用等。也可以是我们产品化后的一些内部检查报错,如我们的4377(dml defensive check error), 4103 (data checksum error)等,需DBA干预恢复 | +| WDIAG | LOG_WARN | Warning Diagnosis, 协助故障排查的诊断信息,预期内的错误,如函数返回失败。级别与WARN相同 | +| EDIAG | LOG_ERROR | Error Diagnosis, 协助故障排查的诊断信息,非预期的逻辑错误,如函数参数不符合预期等,通常为OceanBase程序BUG。级别与ERROR相同 | + + +> 这里仅介绍了最常用的日志级别,更详细的信息参考 `ob_parameter_seed.ipp` 中关于 `syslog_level` 的配置,以及`ob_log_module.h` 文件中 `LOG_ERROR` 等宏定义。 + +**如何设置日志级别?** + +有三种方式调整日志级别: + +- OceanBase 程序在启动时会读取配置文件或通过命令行参数输入系统日志级别配置,配置项名称是`syslog_level`。 +- 启动后也可以通过MySQL客户端连接,执行SQL命令 `alter system set syslog_level='DEBUG`; +- 通过SQL HINT修改执行请求时的日志级别,比如 `select /*+ log_level("ERROR") */ * from foo;`。这种方式只对当前SQL请求相关的日志有效。 + +动态修改日志的代码可以参考 `ObReloadConfig::reload_ob_logger_set`。 + +```cpp +if (OB_FAIL(OB_LOGGER.parse_set(conf_->syslog_level, + static_cast(STRLEN(conf_->syslog_level)), + (conf_->syslog_level).version()))) { + OB_LOG(ERROR, "fail to parse_set syslog_level", + K(conf_->syslog_level.str()), K((conf_->syslog_level).version()), K(ret)); +``` + +## 如何打印日志 + +常见的系统会使用C++ stream方式或C fprintf风格打印日志,但是OceanBase略有不同。接下来从示例入手看如何打印日志。 + +### 打印日志的例子 + +与fprintf不一样,OceanBase的系统日志没有formt string,而只有"info"参数,和各个参数信息。比如: + +```cpp +LOG_INFO("start stmt", K(ret), + K(auto_commit), + K(session_id), + K(snapshot), + K(savepoint), + KPC(tx_desc), + K(plan_type), + K(stmt_type), + K(has_for_update), + K(query_start_time), + K(use_das), + K(nested_level), + KPC(session), + K(plan), + "consistency_level_in_plan_ctx", plan_ctx->get_consistency_level(), + K(trans_result)); +``` + +其中 `"start stmt"` 就是INFO信息,而后面通常使用 `K` 宏与对象。 + +### 日志各字段介绍 + +上面示例代码的一个输出: + +```textile +[2023-12-11 18:00:55.711877] INFO [SQL.EXE] start_stmt (ob_sql_trans_control.cpp:619) +[99178][T1004_TeRec][T1003][YD9F97F000001-00060C36119D4757-0-0] [lt=15] +start stmt(ret=0, auto_commit=true, session_id=1, +snapshot={this:0x7f3184fca0e8, valid:true, source:2, +core:{version:{val:1702288855549635029, v:0}, tx_id:{txid:167035}, +scn:1702288855704049}, uncertain_bound:0, snapshot_lsid:{id:1}, +snapshot_ls_role:0, parts:[{left:{id:1}, right:491146514786417}]}, +savepoint=1702288855704049, tx_desc={this:0x7f31df697420, +tx_id:{txid:167035}, state:2, addr:"127.0.0.1:55801", tenant_id:1003, +session_id:1, assoc_session_id:1, xid:NULL, xa_mode:"", +xa_start_addr:"0.0.0.0:0", access_mode:0, tx_consistency_type:0, +isolation:1, snapshot_version:{val:18446744073709551615, v:3}, +snapshot_scn:0, active_scn:1702288855704040, op_sn:6, alloc_ts:1702288855706134, +active_ts:1702288855706134, commit_ts:-1, finish_ts:-1, timeout_us:29999942, +lock_timeout_us:-1, expire_ts:1702288885706076, coord_id:{id:-1}, +parts:[{id:{id:1}, addr:"127.0.0.1:55801", epoch:491146514786417, +first_scn:1702288855704043, last_scn:1702288855704048, last_touch_ts:1702288855704044}], +exec_info_reap_ts:1702288855704043, commit_version:{val:18446744073709551615, v:3}, +commit_times:0, commit_cb:null, cluster_id:1, cluster_version:17180065792, +flags_.SHADOW:false, flags_.INTERRUPTED:false, flags_.BLOCK:false, +flags_.REPLICA:false, can_elr:true, cflict_txs:[], abort_cause:0, +commit_expire_ts:0, commit_task_.is_registered():false, ref:2}, +plan_type=1, stmt_type=5, has_for_update=false, query_start_time=1702288855711692, +use_das=false, nested_level=0, session={this:0x7f31de2521a0, id:1, +deser:false, tenant:"sys", tenant_id:1, effective_tenant:"sys", +effective_tenant_id:1003, database:"oceanbase", user:"root@%", +consistency_level:3, session_state:0, autocommit:true, tx:0x7f31df697420}, +plan=0x7f31565ba050, consistency_level_in_plan_ctx=3, +trans_result={incomplete:false, parts:[], touched_ls_list:[], +cflict_txs:[]}) +``` + +> NOTE: 实际日志中一行日志并没有换行,这里为了方便查看增加了换行 + +一条日志中主要包含了以下几个部分: + +| 内容 | 示例 | 描述 | +| -------- | ------------------------------------ | ----------------------------------- | +| 时间 | [2023-12-11 18:00:55.711877] | 日志打印时间 | +| 日志级别 | INFO | 日志级别 | +| 模块 | [SQL.EXE] | 日志打印模块 | +| 函数名称 | start_stmt | 打印日志的函数名称 | +| 代码位置信息 | (ob_sql_trans_control.cpp:619) | 打印日志的代码位置。包含文件名和行号 | +| 线程标识 | [99178][T1004_TeRec] | 打印日志的线程号、线程名称 | +| 租户信息 | [T1003] | 租户信息 | +| Trace ID | [YD9F97F000001-00060C36119D4757-0-0] | 某个请求的全局唯一标识。通常可以通过这个标识获取所有当前请求的所有日志 | +| 打印日志时间 | [lt=15] | 当前线程打印上一条日志花费的时间,单位微秒 | +| 日志信息 | start stmt(...) | 日志信息 | + +### 日志参数常用宏介绍 + +对于开发者来说,我们只需要关心如何输出我们的对象信息。通常我们编写`K(obj)`即可将我们想要的信息输出到日志中。下面介绍一些细节。 + +OceanBase 为了避免format string的一些错误,使用自动识别类型然后序列化来解决这个问题。日志任意参数中会识别为多个Key Value对,其中Key是要打印的字段名称,Value是字段的值。比如上面的示例中的 `"consistency_level_in_plan_ctx", plan_ctx->get_consistency_level()` ,就是打印了一个字段的名称和值,OceanBase 自动识别 Value 的类型并转换为字符串。 + +因为大部分日志都是打印对象的某个字段,所以OceanBase提供了一些宏来简化打印日志的操作。最常用的就是 `K`,以上面的例子 `K(ret)`,其展开后在代码中是: + +```cpp +"ret", ret +``` + +最终展示在日志中是: +```cpp +ret=-5595 +``` + +OceanBase 还提供了一些其它的宏,在不同的场景下使用不同的宏。 + +> 日志参数宏定义可以在 `ob_log_module.h` 文件中找到。 + +| 宏 | 示例 | 说明 | +| --------------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| K | K(ret) | 展开后就是 `"ret", ret`。参数可以是一个简单值,也可以是一个普通对象 | +| K_ | K_(consistency_level) | 展开后是 `"consistency_level", consistency_level_`。与 K 不同的是,会在展开的 Value 后自动增加 `_`后缀,用在类成员变量的打印 | +| KR | KR(ret) | 展开后是 `"ret", ret, "ret", common::ob_error_name(ret)`。这个宏是为了方便打印错误码与错误码名称。在 OceanBase 中,通常使用 `ret` 作为函数的返回值,而每个返回值会有一个对应的字符串描述。`ob_error_name` 就可以获得错误码对应的字符串描述。注意,这个宏只能用在非lib代码中 | +| KCSTRING/
KCSTRING_ | KCSTRING(consistency_level_name) | 展开后是 `"consistency_level_name", consistency_level_name`。这个宏是为了打印 C 格式的字符串。由于`const char *` 类型的变量在C++中未必表示一个字符串,比如一个二进制buffer,那么打印这个变量的值时,当做C字符串来打印输出的话,会遇到访问非法内存的错误,所以增加了这个宏,用来明确表示打印C字符串 | +| KP/KP_ | KP(plan) | 展开后是 `"plan", plan`,其中 `plan` 是一个指针。这个宏将会打印出某个指针的十六进制值 | +| KPC/KPC_ | KPC(session) | 输入参数是对象指针。如果是NULL时会输出NULL,否则调用指针的to_string方法输出字符串 | +| KTIME | KTIME(cur_time) | 时间戳转换为字符串。时间戳单位微秒 | +| KTIMERANGE/
KTIMERANGE_ | KTIMERANGE(cur_time, HOUR, SECOND) | 时间戳转换为字符串,仅获取指定范围,比如示例中的获取小时到秒这一段 | +| KPHEX/KPHEX_ | KPHEX(buf, 20) | 十六进制打印buf内容 | +| KERRMSG | KERRMSG | 输出系统错误码信息 | +| KERRNOMSG | KERRNOMSG(2) | 指定错误码输出系统错误信息 | + +## 日志中的一些实现细节 + +### 值如何转换为字符串 + +OceanBase 会自动识别日志中想要打印的值的类型,然后将其转换为字符串。比如上面的例子中,`ret` 是一个 `int` 类型的变量,而 `plan_ctx->get_consistency_level()` 返回的是一个 `enum` 类型的变量,这两个变量都会被转换为字符串。 + +不过由于OceanBase不知道如何将一个普通对象转换为字符串,所以需要用户自己实现一个 `TO_STRING_KV` 函数,来将对象转换为字符串。比如上面的例子中,`snapshot` 是一个 `ObTxReadSnapshot` 类型的对象,这个对象实现了 `TO_STRING_KV` 函数,所以可以直接打印。 + +**普通值转换为字符串** + + +OceanBase 可以自动识别简单类型的值,比如`int`、`int64_t`、`double`、`bool`、`const char *` 等,将其转换为字符串。对于枚举类型,会当做数字来处理。对于指针,将会使用十六进制的方式输出指针值。 + + + +**类对象转换为字符串** + + +由于C++没有反射机制,所以无法自动识别一个类对象的成员变量,将其转换为字符串。所以需要用户自己实现一个 `TO_STRING_KV` 函数,来将对象转换为字符串。比如上面的例子中,`snapshot` 是一个 `ObTxReadSnapshot` 类型的对象,这个对象实现了 `TO_STRING_KV` 函数,可以参考实现代码如下: + +```cpp +class ObTxReadSnapshot { + ... + TO_STRING_KV(KP(this), + K_(valid), + K_(source), + K_(core), + K_(uncertain_bound), + K_(snapshot_lsid), + K_(parts)); +}; +``` + +可以看到,在 `TO_STRING_KV` 直接使用与打印日志类似的宏,"列出"想要输出的成员变量名称即可。 + +> NOTE: TO_STRING_KV 其实是一个宏定义,具体实现可以参考 ob_print_utils.h。 TO_STRING_KV 将输入参数转换为字符串输出到一个buffer中。 + + + +### 日志模块 + +OceanBase 的日志是区分模块的,而且可以支持子模块。比如上面的例子中,`[SQL.EXE]` 就是一个模块,`SQL` 是一个主模块,`EXE` 是一个子模块。日志模块的定义可以参考 `ob_log_module.h` 文件中 `LOG_MOD_BEGIN` 和 `DEFINE_LOG_SUB_MOD` 相关的代码。 + + + +**日志模块是如何输出到日志中的?** + + +通常情况下,我们仅仅使用像`LOG_WARN`这样的宏来打印日志,就会输出不同的模块,这也是通过宏定义来实现的。仍然以上面的日志为例,在 `ob_sql_trans_control.cpp` 文件的开始可以看到一个宏定义 `#define USING_LOG_PREFIX SQL_EXE`,这个宏定义了当前文件的日志模块,也就是说,当前文件中的所有日志都会打印 `[SQL.EXE]` 这个模块。 + +> 这里也有一个缺陷,就是当前实现文件中引入的头文件,默认也会使用这个模块来打印日志。 + + + +**如何明确的指定模块名称?** + + +使用上面的方式确实有些不够灵活,OceanBase 还有另一种方式来指定模块名称,即使用宏 `OB_MOD_LOG` 或 `OB_SUB_MOD_LOG`。这两个宏的使用方法与 `LOG_WARN` 类似,只是多了模块参数与日志级别: + +```cpp +OB_MOD_LOG(parMod, level, info_string, args...) +OB_SUB_MOD_LOG(parMod, subMod, level, info_string, args...) +``` + +**设置模块的日志级别** +OceanBase 除了可以设置全局和当前线程的日志级别,还可以调整某个模块的日志级别。当前可以通过 SQL HINT 的方式修改执行请求时的某个模块的日志级别,比如: + +```sql +select /*+ log_level("SHARE.SCHEMA:ERROR") */ * from foo; +``` + +其中 `SHARE` 是主模块,`SCHEMA` 是子模块,`ERROR` 是日志级别。这个 SQL HINT 的作用是将 `SHARE.SCHEMA` 模块的日志级别设置为 `ERROR`,并且只对当前请求有效。 + +### 日志时间 + +OceanBase 的日志时间是当前本地时间的微秒数。 +由于将时间戳转换为字符串是一个相当耗时的事情,OceanBase 将时间戳转换缓存下来以加速该过程,具体可以参考 `ob_fast_localtime` 函数。 + +### 线程标识 + +当前会记录两个线程相关的信息: + +- 线程号:系统调用 `__NR_gettid` 返回的信息(系统调用比较低效,这个值会缓存下来); +- 线程名称:线程名称字段中可能会包含租户ID、线程池类型和线程池中的编号。OceanBase的线程号是通过 `set_thread_name` 函数设置的,还会显示在 `top` 命令中。 + +> NOTE:线程名称是由创建的线程决定的,由于创建线程的租户可能与此线程后续运行的租户不同,所以线程名称中的租户可能不对。 + +### 日志限速 + +OceanBase 支持两种日志限速:一个普通系统日志磁盘IO带宽限制,一个是WARN系统日志限制。 + + +**系统日志带宽限速** + +OceanBase 会按照磁盘带宽来限制日志输出。日志带宽限速不会针对不同的日志级别限速。如果日志限速,可能会打印限速日志,关键字 `REACH SYSLOG RATE LIMIT `。 + +限速日志示例: + +```txt +[2023-12-26 09:46:04.621435] INFO [SHARE.LOCATION] fetch_vtable_location_ (ob_vtable_location_service.cpp:281) [35675][VTblLocAsyncUp0][T0][YB427F000001-00060D52A9614571-0-0] [lt=0] REACH SYSLOG RATE LIMIT [bandwidth] +``` + +可以通过配置项 `syslog_io_bandwidth_limit` 来调整限速。 + +限速的代码细节请参考 `check_tl_log_limiter` 函数。 + +**WDIAG 日志限速** +OceanBase 对WARN级别的日志做了限流,每个错误码每秒钟默认限制输出200条日志。超过限制会输出限流日志,关键字 `Throttled WDIAG logs in last second`。可以通过配置项 `diag_syslog_per_error_limit` 来调整限流阈值。 + +限流日志示例: + +```txt +[2023-12-25 18:01:15.527519] WDIAG [SHARE] refresh (ob_task_define.cpp:402) [35585][LogLimiterRefre][T0][Y0-0000000000000000-0-0] [lt=8][errcode=0] Throttled WDIAG logs in last second(details {error code, dropped logs, earliest tid}=[{errcode:-4006, dropped:31438, tid:35585}]) +``` + +限流代码参考 `ObSyslogPerErrLimiter::do_acquire`。 + + + + +## 一些其它细节 + +### DBA日志 + +OceanBase 中还有两类特殊的日志,LOG_DBA_WARN 和 LOG_DBA_ERROR,分别对应了WARN和ERROR日志。由于OceanBase日志量特别大,并且大部分只有研发人员才能理解,为DBA运维排查问题带来了一定的负担,因此增加这两种日志,希望让DBA仅关注少量的这两类日志即可排查系统问题。而使用 LOG_WARN 和 LOG_ERROR 输出的日志被转换成 WDIAG 和 EDIAG 日志,帮助开发人员排查问题。 + +### 输出提示信息到用户终端 + +有时候我们希望将错误信息直接输出到用户的终端,可以更方便的了解当前发生了什么错误。这时我们可以使用 `LOG_USER_ERROR`、`LOG_USER_WARN`、`LOG_USER_INFO`等宏来打印日志。每个错误码都有一个对应的`USER_ERROR_MSG`,如果这个`USER_ERROR_MSG`需要输入参数,那么我们在打印日志的时候也需要提供对应的参数。比如错误码`OB_NOT_SINGLE_RESOURCE_POOL`,有对应的 `OB_NOT_SINGLE_RESOURCE_POOL__USER_ERROR_MSG`,信息是 "create tenant only support single resource pool now, but pool list is %s",我们就需要提供一个字符串。 + +LOG_USER_ERROR 宏定义如下: + +```cpp +#define LOG_USER_ERROR(errcode, args...) +``` + +其它宏的使用方法类似。 + +> 错误码的定义可以在 `src/share/ob_errno.h` 中找到。 + +由于`LOG_USER_XXX` 提供了固定的错误信息,如果我们想要输出一些自定义的信息,可以使用 `FORWARD_USER_XXX`,比如 `FORWARD_USER_ERROR`,`FORWARD_USER_WARN`等。以 `FORWARD_USER_ERROR`为例,其定义如下: + +```cpp +#define FORWARD_USER_ERROR(errcode, args...) +``` + +### 健康日志 + +OceanBase 会周期性的输出一些内部状态信息,比如各模块、租户的内存信息,到日志中,方便查找问题。这种日志通常一条会输出多行数据,比如: + +```txt +[2023-12-26 13:15:58.608131] INFO [LIB] print_usage (ob_tenant_ctx_allocator.cpp:176) [35582][MemDumpTimer][T0][Y0-0000000000000000-0-0] [lt=116] +[MEMORY] tenant_id= 500 ctx_id= GLIBC hold= 4,194,304 used= 1,209,328 limit= 9,223,372,036,854,775,807 +[MEMORY] idle_size= 0 free_size= 0 +[MEMORY] wash_related_chunks= 0 washed_blocks= 0 washed_size= 0 +[MEMORY] hold= 858,240 used= 575,033 count= 3,043 avg_used= 188 block_cnt= 93 chunk_cnt= 2 mod=glibc_malloc +[MEMORY] hold= 351,088 used= 104,389 count= 3,290 avg_used= 31 block_cnt= 51 chunk_cnt= 1 mod=Buffer +[MEMORY] hold= 1,209,328 used= 679,422 count= 6,333 avg_used= 107 mod=SUMMARY +``` + +这种数据会查找历史问题很有帮助。 + +### ERROR 日志 +对系统出现的一般错误,比如处理某个请求时,出现了异常,会以WARN级别输出日志。只有影响到OceanBase进程正常运行,或者认为有严重问题时,会以ERROR级别输出日志。因此如果遇到进程异常退出,或者无法启动时,搜索ERROR日志会更有效地查找问题原因。 diff --git a/docs/docs/zh/memory.md b/docs/docs/zh/memory.md new file mode 100644 index 000000000..d12b78c68 --- /dev/null +++ b/docs/docs/zh/memory.md @@ -0,0 +1,134 @@ +--- +title: 内存管理 +--- + +# 简介 +内存管理是所有大型C++工程中最重要的模块之一。由于OceanBase还需要处理多租户内存资源隔离问题,因此OceanBase相较于普通的C++工程,内存管理更加复杂。通常,一个良好的内存管理模块需要考虑以下几个问题: + +- 易用。设计的接口比较容器理解和使用,否则代码会很难阅读和维护,也会更容易出现内存错误; +- 高效。高效的内存分配器对性能影响至关重大,尤其是在高并发场景下; +- 诊断。随着代码量的增长,BUG在所难免。常见的内存错误,比如内存泄露、内存越界、野指针等问题让开发和运维都很头疼,如何编写一个能够帮助我们避免或排查这些问题的功能,也是衡量内存管理模块优劣的重要指标。 + +对于多租户模式,内存管理设计的影响主要有以下几个方面: +- 透明的接口设计。如何让开发人员无感、或极少的需要关心不同租户的内存管理工作; +- 高效准确。内存充足应该必须申请成功,租户内存耗尽应该及时察觉,是多租户内存管理的最基础条件。 + +本篇文章将会介绍OceanBase 中常用的内存分配接口与内存管理相关的习惯用法,关于内存管理的技术细节,请参考[内存管理](https://open.oceanbase.com/blog/8501613072)(中文版)。 + +# OceanBase 内存管理常用接口与方式 +OceanBase 针对不同场景,提供了不同的内存分配器。另外为了提高程序执行效率,有一些约定的实现,比如reset/reuse等。 + +## ob_malloc + +OceanBase数据库自研了一套libc风格的接口函数ob_malloc/ob_free/ob_realloc,这套接口会根据tenant_id、ctx_id、label等属性动态申请大小为size的内存块,并且为内存块打上标记,确定归属。这不仅方便了多租户的资源管理,而且对诊断内存问题有很大帮助。 +ob_malloc会根据tenant_id、ctx_id索引到相应的ObTenantCtxAllocator,ObTenantCtxAllocator会按照当前租户上下文环境分配内存。 +ob_free通过偏移运算求出即将释放的内存所对应的对象分配器,再将内存放回内存池。 +ob_realloc与libc的realloc不同,它不是在原有地址上扩容,而是先通过ob_malloc+memcpy将数据复制到另一块内存上,再调用ob_free释放原有内存。 + +```cpp +inline void *ob_malloc(const int64_t nbyte, const ObMemAttr &attr = default_memattr); +inline void ob_free(void *ptr); +inline void *ob_realloc(void *ptr, const int64_t nbyte, const ObMemAttr &attr); +``` + +## OB_NEWx +与 ob_malloc 类似,OB_NEW提供了一套"C++"的接口,在分配释放内存的同时会调用对象的构造析构函数。 + +## ObArenaAllocator +设计特点是多次申请一次释放,只有reset或者析构才真正释放内存,在这之前申请的内存即使主动调用free也不会有任何效用。 +ObArenaAllocator 适用于很多小内存申请,短时间内存会释放的场景。比如一次SQL请求中,会频繁申请很多小内存,并且这些小内存的生命周期会持续整个请求期间。通常情况下,一次SQL的请求处理时间也非常短。这种内存分配方式对于小内存和避免内存泄露上非常有效。在OceanBase的代码中如果遇到只有申请内存却找不到释放内存的地方,不要惊讶。 + +> 代码参考 `page_arena.h` + +## ObMemAttr 介绍 + +OceanBase 使用 `ObMemAttr` 来标记一段内存。 + +```cpp +struct ObMemAttr +{ + uint64_t tenant_id_; // 租户 + ObLabel label_; // 标签、模块 + uint64_t ctx_id_; // 参考 ob_mod_define.h,每个ctx id都会分配一个ObTenantCtxAllocator + uint64_t sub_ctx_id_; // 忽略 + ObAllocPrio prio_; // 优先级 +}; +``` + +> 参考文件 alloc_struct.h + +**tenant_id** + +内存分配管理会按照租户维护进行资源统计、限制。 + +**label** + +在最开始,OceanBase 使用预定义的方式为各个模块创建内存标签。但是随着代码量的增长,预定义标签的方式不太适用,当前改用直接使用常量字符串的方式构造ObLabel。在使用ob_malloc时,也可以直接传入常量字符串当做ObLabel参数。 + +**ctx_id** + +ctx id是预定义的,可以参考 `alloc_struct.h`。每个租户的每个ctx_id都会创建一个`ObTenantCtxAllocator` 对象,可以单独统计相关的内存分配使用情况。通常情况下使用`DEFAULT_CTX_ID`作为ctx id。一些特殊的模块,比如希望更方便的观察内存使用情况或者更方便的排查问题,我们为它定义特殊的ctx id,比如libeasy通讯库(LIBEASY)、Plan Cache缓存使用(PLAN_CACHE_CTX_ID)。我们可以在内存中看到周期性的内存统计信息,比如: + +```txt +[2024-01-02 20:05:50.375549] INFO [LIB] operator() (ob_malloc_allocator.cpp:537) [47814][MemDumpTimer][T0][Y0-0000000000000000-0-0] [lt=10] [MEMORY] tenant: 500, limit: 9,223,372,036,854,775,807 hold: 800,768,000 rpc_hold: 0 cache_hold: 0 cache_used: 0 cache_item_count: 0 +[MEMORY] ctx_id= DEFAULT_CTX_ID hold_bytes= 270,385,152 limit= 2,147,483,648 +[MEMORY] ctx_id= GLIBC hold_bytes= 8,388,608 limit= 9,223,372,036,854,775,807 +[MEMORY] ctx_id= CO_STACK hold_bytes= 106,954,752 limit= 9,223,372,036,854,775,807 +[MEMORY] ctx_id= LIBEASY hold_bytes= 4,194,304 limit= 9,223,372,036,854,775,807 +[MEMORY] ctx_id= LOGGER_CTX_ID hold_bytes= 12,582,912 limit= 9,223,372,036,854,775,807 +[MEMORY] ctx_id= PKT_NIO hold_bytes= 17,969,152 limit= 9,223,372,036,854,775,807 +[MEMORY] ctx_id= SCHEMA_SERVICE hold_bytes= 135,024,640 limit= 9,223,372,036,854,775,807 +[MEMORY] ctx_id= UNEXPECTED_IN_500 hold_bytes= 245,268,480 limit= 9,223,372,036,854,775,807 +``` + +**prio** + +当前支持两个内存分配优先级,Normal和High,默认为Normal,定义可以参考 `enum ObAllocPrio` 文件`alloc_struct.h`。高优先级内存可以从urgent(memory_reserved)内存中分配内存,否则不可以。参考 `AChunkMgr::update_hold`实现。 + +> 可以使用配置项 `memory_reserved` 查看预留内存大小。 + +## init/destroy/reset/reuse + +缓存是提升程序性能的重要手段之一,对象重用也是缓存的一种方式,一方面减少内存申请释放的频率,另一方面可以减少一些构造析构的开销。OceanBase 中有大量的对象重用,并且形成了一些约定,比如reset和reuse函数。 + +**reset** + +用于重置对象。把对象的状态恢复成构造函数或者init函数执行后的状态。比如 `ObNewRow::reset`。 + +**reuse** + +相较于reset,更加轻量。尽量不去释放一些开销较大的资源,比如 `PageArena::reuse`。 + +OceanBase 中还有两个常见的接口是`init`和`destroy`。在构造函数中仅做一些非常轻量级的初始化工作,比如指针初始化为`nullptr`。 + +## SMART_VAR/HEAP_VAR +SMART_VAR是定义局部变量的辅助接口,使用该接口的变量总是优先从栈上分配,当栈内存不足时退化为从堆上分配。对于那些不易优化的大型局部变量(>8K),该接口即保证了常规场景的性能,又能将栈容量安全地降下来。接口定义如下: + +```cpp +SMART_VAR(Type, Name, Args...) { + // do... +} +``` + +满足以下条件时从栈上分配,否则从堆上分配 +```cpp +sizeof(T) < 8K || (stack_used < 256K && stack_free > sizeof(T) + 64K) +``` + +> SMART_VAR 的出现是为了解决历史问题。尽量减少大内存对象占用太多的栈内存。 + +HEAP_VAR 类似于 SMART_VAR,只是它一定会在堆上申请内存。 + +## SMART_CALL +SMART_CALL用于"准透明化"的解决那些在栈非常小的线程上可能会爆栈的递归函数调用。该接口接受一个函数调用为参数,函数调用前会自动检查当前栈的使用情况,一旦发现栈可用空间不足立即在本线程上新建一个栈执行函数,函数结束后继续回到原始栈。即保证了栈足够时的性能,也可以兜底爆栈场景。 + +```cpp +SMART_CALL(func(args...)) +``` + +注意: +1. func返回值必须是表征错误码的int类型 +2. SMART_CALL会返回错误码,这个可能是内部机制的也可能是func调用的 +3. 支持栈级联扩展,每次扩展出一个2M栈(有一个写死的总上限,10M) + +SMART_CALL 相对于直接调用多了 `check_stack_overflow` 栈移除检查。 diff --git a/docs/docs/zh/mysqltest.md b/docs/docs/zh/mysqltest.md new file mode 100644 index 000000000..374b7e576 --- /dev/null +++ b/docs/docs/zh/mysqltest.md @@ -0,0 +1,112 @@ +# 使用 obd.sh 运行 mysqltest + +要使用 obd.sh 运行 mysqltest 测试,需要通过 obd.sh 部署 OceanBase 数据库。本文从编译源代码开始,使用示例介绍如何使用 obd.sh 部署 OceanBase 数据库并运行 mysqltest 测试。 + +## 背景 + +为了简化开发者的操作步骤,降低其理解成本,我们将一些 OBD 命令封装到 obd.sh 脚本中,并将脚本存放在 OceanBase 源代码的 oceanbase/tools/deploy 目录下。本文通过在 OBD 中调用 [obd test mysqltest](https://www.oceanbase.com/docs/community-obd-cn-10000000002048173) 命令,运行 mysqltest 测试。 + +## 相关概念 + +mysqltest 是OceanBase数据库中的一种准入测试,简单来说,它以编写的 case 文件为输入,将数据库的输出与预期输出进行比较。OceanBase数据库中 mysqltest 测试的 case 都位于 `tools/deploy/mysql_test` 目录下。 + +`case` 是 mysqltest 的最小执行单元,一个 `case` 至少包含一个 test 文件和一个 result 文件。对 case 进行分类形成 `suite`,`suite` 是 case 的集合。 + +mysqltest 有多种运行模式,取决于选择的节点,常见的 mysqltest 模式如下。 + +* c 模式:连接到 Primary Zone 所在的服务器运行 mysqltest。例如,使用配置文件 distributed.yaml 部署集群,然后连接到 server1 运行测试。 + + ```shell + ./obd.sh mysqltest -n --suite acs --test-server=server1 + ``` +* Slave 模式:连接到 Primary Zone 以外的服务器运行 mysqltest。例如,使用配置文件 distributed.yaml 部署集群,然后连接到 server2 运行测试。 + + ```shell + ./obd.sh mysqltest -n --suite acs --test-server=server2 + ``` + +* Proxy 模式:通过 ODP 连接到集群进行 mysqltest 测试。例如,使用配置文件 distributed-with-proxy.yaml 部署集群,然后运行测试。 + + ```shell + ./obd.sh mysqltest -n --all + ``` + +## 执行步骤 + +### 步骤 1: 编译源代码 + +查看[编译运行](./build-and-run.md)文档,编译源代码。 + +### 步骤 2: 运行 mysqltest 测试 + +可以全部运行测试,也可以指定测试的 `case` 或 `suite`。具体执行 obd.sh 脚本时使用的参数含义,请参考[附录](#附录)。 + +* 全部测试,即运行 `mysql_test/test_suite` 目录下的所有 suite,请参考以下命令。 + + ```shell + [admin@obtest ~]$ cd oceanbase/tools/deploy + [admin@obtest deploy]$ ./obd.sh mysqltest -n test --all + ``` + +* 指定 case 测试,例如指定 `mysql_test/test_suite/alter/t/alter_log_archive_option.test`。请参考以下命令。 + + ```shell + [admin@obtest ~]$ cd oceanbase/tools/deploy + [admin@obtest deploy]$ ./obd.sh mysqltest -n test --test-dir ./mysql_test/test_suite/alter/t --result-dir ./mysql_test/test_suite/alter/r --test-set alter_log_archive_option + ``` + +* 指定一个 suite 测试,例如在 `mysql_test/test_suite` 目录下指定一个 suite 进行测试,请参考以下命令。 + + ```shell + [admin@obtest ~]$ cd oceanbase/tools/deploy + [admin@obtest deploy]$ ./obd.sh mysqltest -n test --suite acs + ``` + +## 附录 + +在执行 mysqltest 测试时,可以根据实际情况配置一些参数。参数说明如下表: + +| 参数名称 | 是否必须 | 类型 | 默认值 | 说明 | +|--------|---------|----------|-------|------| +| -n | Y | 字符串 | null | 集群名称 | +| --component | N | 字符串 | null | T要测试的组件名称。可选值:obproxy、obproxy-ce、oceanbase和oceanbase-ce。如果是空,按照 obproxy、obproxy-ce、oceanbase、oceanbase-ce的顺序检查。如果检测到某个组件存在,就会停止遍历,然后执行相应的测试。 | +| --test-server | N | 字符串 | 默认为指定组件下的服务器中的第一个节点 | 要测试的机器,可以设置为 yaml 文件中 servers 下指定组件下的 name 值对应的服务器。如果 servers 下没有配置 name 值,将使用 ip 值,必须是指定组件下的某个节点的 name 值。 | +| --user | N | 字符串 | admin | 执行测试的用户名,一般不需要修改。 | +| --password | N | 字符串 | admin | 密码 | +| --database | N | 字符串 | test | database | +| --mysqltest-bin | N | 字符串 | /u01/obclient/bin/mysqltest | mysqltest 二进制文件路径 | +| --obclient-bin | N | 字符串 | obclient | obclient 二进制文件路径 | +| --test-dir | N | 字符串 | ./mysql_test/t | test-file 的目录。如果找不到 test-file,将尝试在 OBD 内置中查找。 | +| --test-file-suffix | N | 字符串 | .test | mysqltest 所需的 test-file 的后缀。 | +| --result-dir | N | 字符串 | ./mysql_test/r | result-file 的目录。如果找不到 result-file,将尝试在 OBD 内置中查找。 | +| --result-file-suffix | N | 字符串 | .result | result-file 文件的后缀。 | +| --record | N | bool | false | 仅记录执行结果,不执行测试。 | +| --record-dir | N | 字符串 | ./record | 执行结果记录的目录。 | +| --record-file-suffix | N | 字符串 | .record | 执行结果记录的文件后缀。 | +| --tmp-dir | N | 字符串 | ./tmp | mysqltest 的 tmpdir 选项。 | +| --var-dir | N | 字符串 | ./var | mysqltest 的 logdir 选项。 | +| --test-set | N | 字符串 | no | 测试用例数组。使用逗号(`,`)分隔多个用例。 | +| --exclude | N | 字符串 | no | 需要排除的测试用例数组。使用逗号(`,`)分隔多个用例。 | +| --test-pattern | N | 字符串 | no | 测试文件匹配的正则表达式。所有匹配表达式的用例将覆盖 test-set 选项。 | +| --suite | N | 字符串 | no | suite 数组。一个 suite 包含多个测试,可以用逗号(`,`)分隔。 | +| --suite-dir | N | 字符串 | ./mysql_test/test_suite | suite 目录。如果找不到 suite 目录,将尝试在 OBD 内置中查找。 | +| --all | N | bool | false | 执行所有 --suite-dir 下的 case。suite 目录所在的目录下的所有 case。 | +| --need-init | N | bool | false | 执行初始化SQL文件。新集群可能需要在执行 mysqltest 之前执行一些初始化文件,例如创建用于 case 的账户和租户。 | +| --init-sql-dir | N | 字符串 | ./ | 初始化SQL文件所在的目录。当找不到 sql 文件时,将尝试在 OBD 内置中查找。 | +| --init-sql-files | N | 字符串 | Default is empty | 初始化SQL文件数组,当需要初始化时执行。使用逗号(`,`)分隔。如果未填写,如果需要初始化,OBD 将根据集群配置执行内置初始化。 | +| --auto-retry | N | bool | false | 测试失败时,自动重新部署集群并重试。 | +| --psmall | N | bool | false | 以psmall模式执行case。 | +| --slices | N | int | null | 测试用例拆分为多少组。 | +| --slice-idx | N | int | null | 指定当前组 ID。 | +| --slb-host | N | 字符串 | null | 指定软负载均衡中心。 | +| --exec-id | N | 字符串 | null | 指定执行 ID。 | +| --case-filter | N | 字符串 | ./mysql_test/filter.py | filter.py 文件维护需要过滤的 case。 | +| --reboot-timeout | N | int | 0 | 重启超时时间。 | +| --reboot-retries | N | int | 5 | 失败重启的重试次数。 | +| --collect-all | N | bool | false | 是否收集组件日志。 | +| --log-dir | N | 字符串 | 日志默认在tmp_dir下 | mysqltest 的日志存储路径。 | +| --log-pattern | N | 字符串 | *.log | 收集与正则表达式匹配的日志文件名称,命中的文件将被收集。 | +| --case-timeout | N | int | 3600 | 单个测试用例的超时时间。 | +| --disable-reboot | N | bool | false | 测试执行期间不再重启。 | +| --collect-components | N | 字符串 | null | 收集哪些组件的日志。多个组件用逗号(`,`)分隔。 | +| --init-only | N | bool | false | 仅指定初始化SQL文件。 | diff --git a/docs/docs/zh/toolchain.md b/docs/docs/zh/toolchain.md new file mode 100644 index 000000000..ca47d9c89 --- /dev/null +++ b/docs/docs/zh/toolchain.md @@ -0,0 +1,59 @@ +# 安装工具链 + +在编译OceanBase源码之前,需要先在开发环境中安装C++工具链。如果你的开发环境中还没有安装C++工具链,可以按照本文档中的指导进行安装。 + +## 支持的操作系统 + +OceanBase 并不支持所有的操作系统,特别是 Windows 和 Mac OS X。 + +这是当前兼容的操作系统列表: + +| 操作系统 | 版本 | 架构 | 是否兼容 | 安装包是否可部署 | 编译的二进制文件是否可部署 | 是否测试过 MYSQLTEST | +| ------------------- | --------------------- | ------ | ---------- | ------------------ | -------------------------- | ---------------- | +| Alibaba Cloud Linux | 2.1903 | x86_64 | Yes | Yes | Yes | Yes | +| CentOS | 7.2 / 8.3 | x86_64 | Yes | Yes | Yes | Yes | +| Debian | 9.8 / 10.9 | x86_84 | Yes | Yes | Yes | Yes | +| Fedora | 33 | x86_84 | Yes | Yes | Yes | Yes | +| openSUSE | 15.2 | x86_84 | Yes | Yes | Yes | Yes | +| OpenAnolis | 8.2 | x86_84 | Yes | Yes | Yes | Yes | +| StreamOS | 3.4.8 | x86_84 | Unknown | Yes | Yes | Unknown | +| SUSE | 15.2 | x86_84 | Yes | Yes | Yes | Yes | +| Ubuntu | 16.04 / 18.04 / 20.04 | x86_84 | Yes | Yes | Yes | Yes | + +> **注意**: +> +> 其它的 Linux 发行版可能也可以工作。如果你验证了 OceanBase 可以在除了上面列出的发行版之外的发行版上编译和部署,请随时提交一个拉取请求来添加它。 + +## 支持的 GLIBC + +OceanBase 和它的依赖项动态链接到 GNU C Library (GLIBC)。GLIBC 共享库的版本限制为小于或等于 2.34。 + +请查看[ISSUE-1337](https://github.com/oceanbase/oceanbase/issues/1337)了解详情。 + +## 安装 + +这个安装指导因操作系统和包管理器的不同而有所不同。以下是一些流行环境的安装指导: + +### Fedora 系统 + +包括 CentOS, Fedora, OpenAnolis, RedHat, UOS 等等。 + +```shell +yum install git wget rpm* cpio make glibc-devel glibc-headers binutils m4 libtool libaio +``` + +### Debian 系统 + +包括 Debian, Ubuntu 等等。 + +```shell +apt-get install git wget rpm rpm2cpio cpio make build-essential binutils m4 +``` + +### SUSE 系统 + +包括 SUSE, openSUSE 等等。 + +```shell +zypper install git wget rpm cpio make glibc-devel binutils m4 +``` diff --git a/docs/docs/zh/unittest.md b/docs/docs/zh/unittest.md new file mode 100644 index 000000000..dd6bbfe82 --- /dev/null +++ b/docs/docs/zh/unittest.md @@ -0,0 +1,99 @@ +# 编写以及运行单测 + +## 如何编译及运行所有单元测试 + +[OceanBase](https://github.com/oceanbase/oceanbase) 有两个单元测试目录。 + +- `unittest` :这是主要的单元测试用例,它测试 `src` 目录中的代码。 + +- `deps/oblib/unittest`: oblib 的测试用例。 + +首先,你需要编译 `unittest`。你需要进入构建目录中的 `unittest` 目录并显式编译。当你构建 oceanbase 项目时,默认不会构建 unittest。例如: + +```bash +bash build.sh --init --make # init and build a debug mode project +cd build_debug/unittest # 或者 cd build_debug/deps/oblib/unittest +make -j4 # build unittest +``` + +接着可以运行 `run_tests.sh` 脚本来运行所有测试用例。 + +## 编译并运行单个测试 + +可以编译并运行单个测试用例。你可以进入 `build_debug` 目录,执行 `make case-name` 来编译特定的测试用例并运行生成的二进制文件。例如: + +```bash +cd build_debug +# **注意**: 不要进入unittest目录 +make -j4 test_chunk_row_store +find . -name "test_chunk_row_store" +# 可以看到 ./unittest/sql/engine/basic/test_chunk_row_store +./unittest/sql/engine/basic/test_chunk_row_store +``` + +## 编写单元测试 + +作为一个 C++ 项目,[OceanBase](https://github.com/oceanbase/oceanbase)使用[google test](https://github.com/google/googletest)作为单元测试框架。 + +OceanBase 使用 `test_xxx.cpp` 作为单元测试文件名。你可以创建一个 `test_xxx.cpp` 文件,并将文件名添加到特定的 `CMakeLists.txt` 文件中。 + +在 `test_xxx.cpp` 文件中,你需要添加头文件 `#include ` 和下面的 main 函数。 + +```cpp +int main(int argc, char **argv) +{ + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} +``` + +接着可以添加一些函数来测试不同的场景。下面是 `test_ra_row_store_projector.cpp` 的一个例子。 + +```cpp +/// +/// TEST 是 google test 的一个宏。 +/// 用来添加一个新的测试函数。 +/// +/// RARowStore 是一个测试套件的名字,alloc_project_fail 是测试的名字。 +/// +TEST(RARowStore, alloc_project_fail) +{ + ObEmptyAlloc alloc; + ObRARowStore rs(&alloc, true); + + /// + /// ASSERT_XXX 是一些测试宏,可以帮助我们判断结果是否符合预期,如果失败会终止测试。 + /// + /// 还有一些其它的测试宏,以 `EXPECT_` 开头,如果失败不会终止测试。 + /// + ASSERT_EQ(OB_SUCCESS, rs.init(100 << 20)); + const int64_t OBJ_CNT = 3; + ObObj objs[OBJ_CNT]; + ObNewRow r; + r.cells_ = objs; + r.count_ = OBJ_CNT; + int64_t val = 0; + for (int64_t i = 0; i < OBJ_CNT; i++) { + objs[i].set_int(val); + val++; + } + + int32_t projector[] = {0, 2}; + r.projector_ = projector; + r.projector_size_ = ARRAYSIZEOF(projector); + + ASSERT_EQ(OB_ALLOCATE_MEMORY_FAILED, rs.add_row(r)); +} +``` + +可以查看文档[google test document](https://google.github.io/googletest/)获取更多关于 `TEST`, `ASSERT` 和 `EXPECT` 的细节。 + +## GitHub CI的单测 + +在合并拉取请求之前,CI将测试您的拉取请求。`Farm`将测试`mysql test`和`unittest`。您可以查看下面的`Details`链接的详细信息。 + +![github ci](images/unittest-github-ci.png) + +![github ci farm 详情](images/unittest-ci-details.png) + +![Farm unittest](images/unittest-unittest.png) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml new file mode 100644 index 000000000..ba76a36b8 --- /dev/null +++ b/docs/mkdocs.yml @@ -0,0 +1,57 @@ +site_name: OceanBase Developer Guide +site_url: https://oceanbase.github.io/oceanbase +repo_url: https://github.com/oceanbase/oceanbase +repo_name: oceanbase/oceanbase +plugins: + - search + #- material-relative-language-selector + #- mdpo + - i18n: + docs_structure: folder + fallback_to_default: true + reconfigure_material: true + reconfigure_search: true + languages: + - locale: en + default: true + name: English + build: true + - locale: zh + name: 中文 + build: true + link: /zh/ + - git-authors +nav: + - README.md + - toolchain.md + - build-and-run.md + - ide-settings.md + - coding-convension.md + - unittest.md + - mysqltest.md + - debug.md + - logging.md + - memory.md + - container.md + - architecture.md + - coding_standard.md + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + +theme: + name: material + language: en + features: + - search.highlight + - navigation.sections + - navigation.path + - navigation.top + logo: assets/logo.png + favicon: assets/favicon.ico + icon: + repo: fontawesome/brands/github diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 000000000..ca2083c83 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +mkdocs-material +mkdocs-static-i18n +mkdocs-git-authors-plugin diff --git a/src/logservice/libobcdc/src/ob_log_rpc.h b/src/logservice/libobcdc/src/ob_log_rpc.h index 80ae88da3..c79b4847d 100644 --- a/src/logservice/libobcdc/src/ob_log_rpc.h +++ b/src/logservice/libobcdc/src/ob_log_rpc.h @@ -35,7 +35,7 @@ class IObLogRpc public: virtual ~IObLogRpc() { } - // Reuest start LSN by timestamp + // Request start LSN by timestamp virtual int req_start_lsn_by_tstamp(const uint64_t tenant_id, const common::ObAddr &svr, obrpc::ObCdcReqStartLSNByTsReq &req, diff --git a/src/logservice/logfetcher/ob_log_rpc.h b/src/logservice/logfetcher/ob_log_rpc.h index e0f2d0007..0e7911696 100644 --- a/src/logservice/logfetcher/ob_log_rpc.h +++ b/src/logservice/logfetcher/ob_log_rpc.h @@ -36,7 +36,7 @@ class IObLogRpc public: virtual ~IObLogRpc() { } - // Reuest start LSN by timestamp + // Request start LSN by timestamp virtual int req_start_lsn_by_tstamp(const uint64_t tenant_id, const common::ObAddr &svr, obrpc::ObCdcReqStartLSNByTsReq &req, diff --git a/src/logservice/logminer/ob_log_miner_logger.cpp b/src/logservice/logminer/ob_log_miner_logger.cpp index cdb3361b3..9e0624cb9 100644 --- a/src/logservice/logminer/ob_log_miner_logger.cpp +++ b/src/logservice/logminer/ob_log_miner_logger.cpp @@ -29,8 +29,15 @@ LogMinerLogger &LogMinerLogger::get_logminer_logger_instance() static LogMinerLogger logger_instance; return logger_instance; } + LogMinerLogger::LogMinerLogger(): - verbose_(false) { memset(pb_str_, '>', sizeof(pb_str_)); } + verbose_(false), + begin_ts_(0), + last_ts_(0), + last_record_num_(0) +{ + memset(pb_str_, '>', sizeof(pb_str_)); +} void LogMinerLogger::log_stdout(const char *format, ...) { @@ -79,8 +86,29 @@ int LogMinerLogger::log_progress(int64_t record_num, int64_t current_ts, int64_t nls_format, 0, time_buf, sizeof(time_buf), pos))) { LOG_WARN("datetime to string failed", K(current_ts), K(LOGMINER_TZ.get_tz_info())); } else { - fprintf(stdout, "\r%s %s %5.1lf%%, written records: %-20jd", time_buf, pb_buf, - progress, record_num); + int64_t current_time = ObTimeUtility::current_time(); + int64_t average_rps = 0; + int64_t current_rps = 0; + int64_t inc_record_num = 0; + if (begin_ts_ == 0) { + begin_ts_ = current_time; + } + if (last_ts_ == 0) { + last_ts_ = current_time; + } + if (current_time - begin_ts_ > 0) { + // calculated in seconds + average_rps = record_num * 1000 * 1000 / (current_time - begin_ts_); + } + if (current_time - last_ts_ > 0) { + inc_record_num = record_num - last_record_num_; + // calculated in seconds + current_rps = inc_record_num * 1000 * 1000 / (current_time - last_ts_); + last_ts_ = current_time; + last_record_num_ = record_num; + } + fprintf(stdout, "\r%s %s %5.1lf%%, written records: %5.1jd, current rps: %5.1jd, average rps: %5.1jd", + time_buf, pb_buf, progress, record_num, current_rps, average_rps); fflush(stdout); } return ret; diff --git a/src/logservice/logminer/ob_log_miner_logger.h b/src/logservice/logminer/ob_log_miner_logger.h index a28d69569..e83d24a6a 100644 --- a/src/logservice/logminer/ob_log_miner_logger.h +++ b/src/logservice/logminer/ob_log_miner_logger.h @@ -43,6 +43,9 @@ private: static const int MAX_SCREEN_WIDTH = 4096; bool verbose_; char pb_str_[MAX_SCREEN_WIDTH]; + int64_t begin_ts_; + int64_t last_ts_; + int64_t last_record_num_; }; } diff --git a/src/share/io/ob_io_struct.h b/src/share/io/ob_io_struct.h index c4402af26..78af5f618 100644 --- a/src/share/io/ob_io_struct.h +++ b/src/share/io/ob_io_struct.h @@ -326,8 +326,8 @@ private: class ObDeviceChannel; /** - * worker to process sync io reuest and get result of async io request from file system - * io channel has two independent threads, one for sync io operation, anather for polling events from file system + * worker to process sync io request and get result of async io request from file system + * io channel has two independent threads, one for sync io operation, another for polling events from file system */ class ObIOChannel : public lib::TGRunnable { diff --git a/tools/deploy/obd.sh b/tools/deploy/obd.sh index e05f7eb32..5f10c05c9 100755 --- a/tools/deploy/obd.sh +++ b/tools/deploy/obd.sh @@ -75,7 +75,7 @@ function variables_prepare { function copy_sh { if [[ -f copy.sh ]] then - sh copy.sh $BUILD_PATH + bash copy.sh $BUILD_PATH else echo 'can not find copy.sh' fi