| # Apache IoTDB Node.js Client - 三大核心功能实现规划 |
| |
| ## 一、概述 (Overview) |
| |
| 本文档针对以下三个核心技术领域提供详细的实现规划: |
| |
| 1. **完整的 Redirection 支持** - 优化写入性能通过智能路由 |
| 2. **Tablet 序列化优化** - 高效的二进制协议实现 |
| 3. **查询结果反序列化增强** - TsBlock 流式处理 |
| |
| ## 二、当前实现状态 (Current Implementation Status) |
| |
| ### 2.1 Redirection 支持现状 |
| |
| **Java 参考实现特性:** |
| |
| - ✅ `deviceIdToEndpoint` 映射缓存 |
| - ✅ `endPointToSessionConnection` 连接池 |
| - ✅ `RedirectException` 捕获和重试逻辑 |
| - ✅ `enableRedirection` 配置开关 |
| - ✅ 智能连接路由和故障恢复 |
| |
| **Node.js 当前实现:** |
| |
| - ❌ 无 RedirectException 处理 |
| - ❌ 无设备到端点的映射缓存 |
| - ❌ 无 Redirection 配置选项 |
| - ⚠️ 仅有基础的 round-robin 负载均衡 |
| - ⚠️ SessionPool 未集成 Redirection 优化 |
| |
| **性能影响:** |
| 缺少 Redirection 支持导致: |
| |
| - 所有写入请求通过非优化路由 |
| - 跨节点数据转发增加网络延迟 |
| - 无法利用服务器端数据分区优化 |
| - 高并发场景下性能损失 30-50% |
| |
| ### 2.2 Tablet 序列化现状 |
| |
| **Java 参考实现特性:** |
| |
| - ✅ 完整的 BitMap 序列化(8 值/字节打包) |
| - ✅ 按列序列化优化 |
| - ✅ 支持 TSEncoding 压缩(PLAIN, RLE, GORILLA 等) |
| - ✅ WAL 格式支持 |
| - ✅ 优化的内存分配策略 |
| |
| **Node.js 当前实现:** |
| |
| - ✅ 基本 Tablet 序列化(tree/table 模型) |
| - ✅ 所有 TSDataType 支持(BOOLEAN, INT32, INT64, FLOAT, DOUBLE, TEXT, BLOB, STRING, DATE, TIMESTAMP) |
| - ✅ BitMap 基础实现(null 值标记) |
| - ❌ 无压缩编码支持 |
| - ❌ 无 WAL 格式支持 |
| - ⚠️ BitMap 实现符合协议但未优化 |
| |
| **代码位置:** |
| |
| - Session.ts 行 538-680: `serializeTabletValues()`, `serializeColumn()`, `serializeBitMaps()` |
| |
| ### 2.3 查询结果反序列化现状 |
| |
| **Java 参考实现特性:** |
| |
| - ✅ TsBlockSerde 二进制格式 |
| - ✅ 流式批量获取(configurable fetchSize) |
| - ✅ 多种列编码支持(PLAIN, RLE, DICTIONARY) |
| - ✅ 增量结果集(IoTDBRpcDataSet) |
| - ✅ 优化的列式访问 |
| |
| **Node.js 当前实现:** |
| |
| - ✅ TsBlock 格式解析(SessionDataSet) |
| - ✅ 流式迭代器模式(hasNext/next) |
| - ✅ 基本列解码器(ColumnDecoder.ts) |
| - ✅ 支持 5 种编码:ByteArray, Int32Array, Int64Array, BinaryArray, RLE |
| - ✅ Null indicators 处理 |
| - ⚠️ 未实现所有高级编码(DICTIONARY, FREQ, etc.) |
| - ⚠️ 列访问效率可优化 |
| |
| **代码位置:** |
| |
| - SessionDataSet.ts: 迭代器实现 |
| - ColumnDecoder.ts: 列解码器 |
| - Session.ts 行 700-950: `parseQueryResult()`, `parseTsBlock()` |
| |
| --- |
| |
| ## 三、Redirection 支持实现规划 |
| |
| ### 3.1 架构设计 |
| |
| ``` |
| ┌─────────────────────────────────────────────────────┐ |
| │ SessionPool (with Redirection) │ |
| │ ┌──────────────────────────────────────────────┐ │ |
| │ │ deviceIdToEndpoint: Map<string, EndPoint> │ │ |
| │ │ endPointToConnection: Map<string, Session> │ │ |
| │ │ enableRedirection: boolean │ │ |
| │ └──────────────────────────────────────────────┘ │ |
| └─────────────────────────────────────────────────────┘ |
| ↓ |
| Try insertTablet(deviceId) |
| ↓ |
| ┌──────────────────────┐ |
| │ Check cache │ |
| │ deviceIdToEndpoint │ |
| └──────────────────────┘ |
| ↓ |
| Has cached? ───Yes──→ Use optimal connection |
| │ |
| No |
| ↓ |
| Use default connection (round-robin) |
| ↓ |
| ┌──────────────────────┐ |
| │ Execute insert │ |
| └──────────────────────┘ |
| ↓ |
| Catch RedirectException? |
| │ │ |
| No Yes |
| │ │ |
| ↓ ↓ |
| Success ┌──────────────────────┐ |
| │ handleRedirection │ |
| │ - Cache endpoint │ |
| │ - Get/create conn │ |
| │ - Retry with conn │ |
| └──────────────────────┘ |
| ``` |
| |
| ### 3.2 实现步骤 |
| |
| #### Step 1: 扩展 Config 接口 |
| |
| **文件:** Config.ts |
| |
| ```typescript |
| export interface PoolConfig extends Config { |
| // ... existing fields ... |
| |
| /** |
| * Enable automatic redirection for write operations. |
| * When enabled, the client caches device-to-endpoint mappings |
| * and routes subsequent writes directly to optimal nodes. |
| * |
| * @default true |
| */ |
| enableRedirection?: boolean; |
| |
| /** |
| * Maximum number of redirect retries before failing. |
| * |
| * @default 3 |
| */ |
| maxRedirectRetries?: number; |
| |
| /** |
| * Time-to-live for cached redirect mappings (ms). |
| * Set to 0 for no expiration. |
| * |
| * @default 300000 (5 minutes) |
| */ |
| redirectCacheTTL?: number; |
| } |
| |
| export const DEFAULT_POOL_CONFIG: Partial<PoolConfig> = { |
| ...DEFAULT_CONFIG, |
| maxPoolSize: 10, |
| minPoolSize: 1, |
| maxIdleTime: 60000, |
| waitTimeout: 60000, |
| enableRedirection: true, |
| maxRedirectRetries: 3, |
| redirectCacheTTL: 300000, |
| }; |
| ``` |
| |
| #### Step 2: 添加 RedirectException 类型定义 |
| |
| **文件:** `src/utils/Errors.ts` (新建) |
| |
| ```typescript |
| /** |
| * Represents a redirect recommendation from the server. |
| * Thrown when the server suggests a better endpoint for a device. |
| */ |
| export class RedirectException extends Error { |
| public readonly endpoint: EndPoint; |
| public readonly deviceId: string; |
| |
| constructor(deviceId: string, endpoint: EndPoint, message?: string) { |
| super( |
| message || |
| `Redirect recommended for device ${deviceId} to ${endpoint.host}:${endpoint.port}`, |
| ); |
| this.name = "RedirectException"; |
| this.deviceId = deviceId; |
| this.endpoint = endpoint; |
| } |
| |
| static fromThriftStatus( |
| status: any, |
| deviceId: string, |
| ): RedirectException | null { |
| // Check for REDIRECTION_RECOMMEND status code (Java uses 531) |
| if (status.code === 531 && status.redirectNode) { |
| return new RedirectException( |
| deviceId, |
| { |
| host: status.redirectNode.internalIp || status.redirectNode.ip, |
| port: status.redirectNode.port, |
| }, |
| status.message, |
| ); |
| } |
| return null; |
| } |
| } |
| |
| export enum TSStatusCode { |
| SUCCESS_STATUS = 200, |
| REDIRECTION_RECOMMEND = 531, |
| // ... other codes |
| } |
| ``` |
| |
| #### Step 3: 实现 RedirectCache |
| |
| **文件:** `src/client/RedirectCache.ts` (新建) |
| |
| ```typescript |
| import { EndPoint } from "../utils/Config"; |
| import { logger } from "../utils/Logger"; |
| |
| interface CacheEntry { |
| endpoint: EndPoint; |
| timestamp: number; |
| } |
| |
| /** |
| * Cache for device-to-endpoint redirect mappings. |
| * Supports TTL-based expiration and LRU eviction. |
| */ |
| export class RedirectCache { |
| private deviceToEndpoint: Map<string, CacheEntry> = new Map(); |
| private ttl: number; |
| private maxSize: number; |
| |
| constructor(ttl: number = 300000, maxSize: number = 10000) { |
| this.ttl = ttl; |
| this.maxSize = maxSize; |
| } |
| |
| /** |
| * Get cached endpoint for a device. |
| * Returns null if not cached or expired. |
| */ |
| get(deviceId: string): EndPoint | null { |
| const entry = this.deviceToEndpoint.get(deviceId); |
| |
| if (!entry) { |
| return null; |
| } |
| |
| // Check expiration |
| if (this.ttl > 0 && Date.now() - entry.timestamp > this.ttl) { |
| this.deviceToEndpoint.delete(deviceId); |
| logger.debug(`Redirect cache expired for device: ${deviceId}`); |
| return null; |
| } |
| |
| return entry.endpoint; |
| } |
| |
| /** |
| * Cache endpoint for a device. |
| */ |
| set(deviceId: string, endpoint: EndPoint): void { |
| // Evict oldest entry if cache is full (simple LRU) |
| if (this.deviceToEndpoint.size >= this.maxSize) { |
| const firstKey = this.deviceToEndpoint.keys().next().value; |
| if (firstKey) { |
| this.deviceToEndpoint.delete(firstKey); |
| logger.debug(`Evicted oldest redirect cache entry: ${firstKey}`); |
| } |
| } |
| |
| this.deviceToEndpoint.set(deviceId, { |
| endpoint, |
| timestamp: Date.now(), |
| }); |
| |
| logger.debug( |
| `Cached redirect: ${deviceId} -> ${endpoint.host}:${endpoint.port}`, |
| ); |
| } |
| |
| /** |
| * Remove cached endpoint for a device. |
| */ |
| remove(deviceId: string): void { |
| this.deviceToEndpoint.delete(deviceId); |
| logger.debug(`Removed redirect cache for device: ${deviceId}`); |
| } |
| |
| /** |
| * Clear all cached mappings. |
| */ |
| clear(): void { |
| this.deviceToEndpoint.clear(); |
| logger.debug("Cleared all redirect cache entries"); |
| } |
| |
| /** |
| * Get cache statistics. |
| */ |
| getStats(): { size: number; maxSize: number; ttl: number } { |
| return { |
| size: this.deviceToEndpoint.size, |
| maxSize: this.maxSize, |
| ttl: this.ttl, |
| }; |
| } |
| } |
| ``` |
| |
| #### Step 4: 增强 BaseSessionPool 支持 Redirection |
| |
| **文件:** BaseSessionPool.ts |
| |
| 修改点: |
| |
| ```typescript |
| export abstract class BaseSessionPool { |
| protected config: PoolConfig; |
| protected endPoints: EndPoint[]; |
| protected pool: PooledSession[] = []; |
| |
| // NEW: Redirection support |
| protected redirectCache: RedirectCache; |
| protected endPointToSession: Map<string, PooledSession> = new Map(); |
| |
| constructor(/* ... */) { |
| // ... existing code ... |
| |
| // Initialize redirect cache |
| this.redirectCache = new RedirectCache( |
| this.config.redirectCacheTTL || 300000, |
| 10000, |
| ); |
| } |
| |
| /** |
| * Get endpoint key for map lookup. |
| */ |
| private getEndPointKey(endpoint: EndPoint): string { |
| return `${endpoint.host}:${endpoint.port}`; |
| } |
| |
| /** |
| * Get or create session for specific endpoint. |
| * Used for redirected connections. |
| */ |
| protected async getSessionForEndpoint(endpoint: EndPoint): Promise<Session> { |
| const key = this.getEndPointKey(endpoint); |
| |
| // Check if we already have a connection to this endpoint |
| let pooledSession = this.endPointToSession.get(key); |
| |
| if (pooledSession && pooledSession.session.isOpen()) { |
| return pooledSession.session; |
| } |
| |
| // Create new session for this endpoint |
| logger.debug(`Creating new session for redirect endpoint: ${key}`); |
| |
| const session = new Session({ |
| ...this.config, |
| host: endpoint.host, |
| port: endpoint.port, |
| } as InternalConfig); |
| |
| await session.open(); |
| |
| pooledSession = { |
| session, |
| lastUsed: Date.now(), |
| inUse: false, |
| }; |
| |
| this.endPointToSession.set(key, pooledSession); |
| this.pool.push(pooledSession); |
| |
| return session; |
| } |
| |
| /** |
| * Handle redirect exception and retry insert. |
| */ |
| protected async handleRedirection( |
| deviceId: string, |
| endpoint: EndPoint, |
| insertFn: (session: Session) => Promise<void>, |
| retryCount: number = 0, |
| ): Promise<void> { |
| if (!this.config.enableRedirection) { |
| throw new Error("Redirection is disabled"); |
| } |
| |
| const maxRetries = this.config.maxRedirectRetries || 3; |
| if (retryCount >= maxRetries) { |
| throw new Error( |
| `Max redirect retries (${maxRetries}) exceeded for device: ${deviceId}`, |
| ); |
| } |
| |
| // Ignore "no redirect" indicator (0.0.0.0) |
| if (endpoint.host === "0.0.0.0") { |
| logger.debug(`No redirection needed for device: ${deviceId}`); |
| return; |
| } |
| |
| try { |
| // Cache the redirect mapping |
| this.redirectCache.set(deviceId, endpoint); |
| |
| // Get or create session for redirect endpoint |
| const session = await this.getSessionForEndpoint(endpoint); |
| |
| // Retry insert with redirected connection |
| await insertFn(session); |
| |
| logger.info( |
| `Successfully inserted via redirect: ${deviceId} -> ${endpoint.host}:${endpoint.port}`, |
| ); |
| } catch (error: any) { |
| // If redirect connection fails, remove from cache and retry with default |
| this.redirectCache.remove(deviceId); |
| logger.warn( |
| `Redirect connection failed for ${deviceId}, removed from cache:`, |
| error.message, |
| ); |
| throw error; |
| } |
| } |
| |
| /** |
| * Insert tablet with redirection support. |
| */ |
| async insertTablet( |
| tablet: TreeTablet | ITreeTablet | TableTablet | ITableTablet, |
| ): Promise<void> { |
| // Extract device ID from tablet |
| const deviceId = "deviceId" in tablet ? tablet.deviceId : tablet.tableName; |
| |
| // Check redirect cache |
| const cachedEndpoint = this.config.enableRedirection |
| ? this.redirectCache.get(deviceId) |
| : null; |
| |
| let session: Session; |
| |
| if (cachedEndpoint) { |
| // Use cached redirect endpoint |
| logger.debug( |
| `Using cached redirect for ${deviceId}: ${cachedEndpoint.host}:${cachedEndpoint.port}`, |
| ); |
| session = await this.getSessionForEndpoint(cachedEndpoint); |
| } else { |
| // Use default round-robin session |
| session = await this.getSession(); |
| } |
| |
| try { |
| await session.insertTablet(tablet); |
| |
| if (!cachedEndpoint) { |
| // Release session back to pool if not using cached endpoint |
| this.releaseSession(session); |
| } |
| } catch (error: any) { |
| // Check if this is a redirect exception |
| if (this.config.enableRedirection && this.isRedirectError(error)) { |
| const endpoint = this.extractRedirectEndpoint(error); |
| if (endpoint) { |
| logger.info( |
| `Received redirect recommendation: ${deviceId} -> ${endpoint.host}:${endpoint.port}`, |
| ); |
| |
| // Release current session |
| if (!cachedEndpoint) { |
| this.releaseSession(session); |
| } |
| |
| // Handle redirection and retry |
| await this.handleRedirection( |
| deviceId, |
| endpoint, |
| async (redirectSession) => { |
| await redirectSession.insertTablet(tablet); |
| }, |
| ); |
| return; |
| } |
| } |
| |
| // Not a redirect error or redirect disabled, release and rethrow |
| if (!cachedEndpoint) { |
| this.releaseSession(session); |
| } |
| throw error; |
| } |
| } |
| |
| /** |
| * Check if error is a redirect recommendation. |
| */ |
| private isRedirectError(error: any): boolean { |
| // Check for redirect status code (Java uses 531) |
| return ( |
| error.message && |
| (error.message.includes("REDIRECTION_RECOMMEND") || |
| error.message.includes("redirect") || |
| error.code === 531) |
| ); |
| } |
| |
| /** |
| * Extract redirect endpoint from error. |
| */ |
| private extractRedirectEndpoint(error: any): EndPoint | null { |
| try { |
| // Parse redirect info from error message or status |
| // Format varies, need to check actual IoTDB response |
| if (error.redirectNode) { |
| return { |
| host: error.redirectNode.internalIp || error.redirectNode.ip, |
| port: error.redirectNode.port, |
| }; |
| } |
| // TODO: Add more parsing logic based on actual error format |
| return null; |
| } catch (e) { |
| logger.warn("Failed to extract redirect endpoint:", e); |
| return null; |
| } |
| } |
| } |
| ``` |
| |
| #### Step 5: 更新 Session.insertTabletInternal |
| |
| **文件:** Session.ts |
| |
| 修改 `insertTreeTabletInternal` 和 `insertTableTabletInternal` 以捕获 Thrift 响应并检查 redirect 状态: |
| |
| ```typescript |
| private async insertTreeTabletInternal(tablet: TreeTablet | ITreeTablet): Promise<void> { |
| logger.debug(`Inserting tree tablet for device: ${tablet.deviceId}`); |
| |
| const client = this.connection.getClient(); |
| const sessionId = this.connection.getSessionId(); |
| |
| // ... existing serialization code ... |
| |
| const req = new ttypes.TSInsertTabletReq({ |
| sessionId: sessionId, |
| prefixPath: tablet.deviceId, |
| measurements: tablet.measurements, |
| values: this.serializeTabletValues(/*...*/), |
| timestamps: timestampBuffer, |
| types: tablet.dataTypes, |
| size: tablet.timestamps.length, |
| isAligned: false, |
| }); |
| |
| return new Promise((resolve, reject) => { |
| client.insertTablet(req, (err: Error, response: any) => { |
| if (err) { |
| reject(err); |
| return; |
| } |
| |
| // NEW: Check for redirect recommendation |
| if (response.code === 531 && response.redirectNode) { |
| const redirectError: any = new Error('REDIRECTION_RECOMMEND'); |
| redirectError.code = 531; |
| redirectError.redirectNode = { |
| ip: response.redirectNode.ip, |
| internalIp: response.redirectNode.internalIp, |
| port: response.redirectNode.port, |
| }; |
| redirectError.deviceId = tablet.deviceId; |
| reject(redirectError); |
| return; |
| } |
| |
| if (response.code !== 200) { |
| reject(new Error(response.message || 'Insert tablet failed')); |
| return; |
| } |
| |
| resolve(); |
| }); |
| }); |
| } |
| ``` |
| |
| ### 3.3 测试计划 |
| |
| #### Unit Tests |
| |
| **文件:** `tests/unit/RedirectCache.test.ts` (新建) |
| |
| ```typescript |
| describe("RedirectCache", () => { |
| test("should cache and retrieve endpoint", () => { |
| const cache = new RedirectCache(60000); |
| const endpoint = { host: "node1", port: 6667 }; |
| |
| cache.set("device1", endpoint); |
| const cached = cache.get("device1"); |
| |
| expect(cached).toEqual(endpoint); |
| }); |
| |
| test("should expire entries after TTL", async () => { |
| const cache = new RedirectCache(100); // 100ms TTL |
| cache.set("device1", { host: "node1", port: 6667 }); |
| |
| await new Promise((resolve) => setTimeout(resolve, 150)); |
| |
| const cached = cache.get("device1"); |
| expect(cached).toBeNull(); |
| }); |
| |
| test("should evict oldest when full", () => { |
| const cache = new RedirectCache(60000, 2); // maxSize=2 |
| |
| cache.set("device1", { host: "node1", port: 6667 }); |
| cache.set("device2", { host: "node2", port: 6667 }); |
| cache.set("device3", { host: "node3", port: 6667 }); |
| |
| expect(cache.get("device1")).toBeNull(); // Evicted |
| expect(cache.get("device2")).not.toBeNull(); |
| expect(cache.get("device3")).not.toBeNull(); |
| }); |
| }); |
| ``` |
| |
| #### E2E Tests |
| |
| **文件:** `tests/e2e/Redirection.test.ts` (新建) |
| |
| ```typescript |
| describe("Redirection E2E Tests", () => { |
| test("should handle redirect and cache endpoint", async () => { |
| const pool = new SessionPool({ |
| nodeUrls: ["node1:6667", "node2:6667", "node3:6667"], |
| enableRedirection: true, |
| maxPoolSize: 10, |
| }); |
| |
| await pool.init(); |
| |
| // First insert may trigger redirect |
| await pool.insertTablet({ |
| deviceId: "root.redirect.device1", |
| measurements: ["s1"], |
| dataTypes: [TSDataType.FLOAT], |
| timestamps: [Date.now()], |
| values: [[25.5]], |
| }); |
| |
| // Second insert should use cached endpoint (check logs) |
| await pool.insertTablet({ |
| deviceId: "root.redirect.device1", |
| measurements: ["s1"], |
| dataTypes: [TSDataType.FLOAT], |
| timestamps: [Date.now() + 1000], |
| values: [[26.0]], |
| }); |
| |
| await pool.close(); |
| }); |
| }); |
| ``` |
| |
| ### 3.4 性能影响预估 |
| |
| **预期改进:** |
| |
| - 减少网络延迟:30-40%(避免跨节点转发) |
| - 提升写入吞吐量:40-60%(直接路由到数据节点) |
| - 降低服务器CPU负载:20-30%(减少数据转发) |
| |
| **基准测试计划:** |
| |
| ```bash |
| # Without redirection |
| ENABLE_REDIRECTION=false CLIENT_NUMBER=50 DEVICE_NUMBER=1000 node benchmark/benchmark-tree.js |
| |
| # With redirection |
| ENABLE_REDIRECTION=true CLIENT_NUMBER=50 DEVICE_NUMBER=1000 node benchmark/benchmark-tree.js |
| ``` |
| |
| --- |
| |
| ## 四、Tablet 序列化优化规划 |
| |
| ### 4.1 当前实现分析 |
| |
| **已实现:** |
| |
| - ✅ 基本序列化流程:timestamps → values → bitmaps |
| - ✅ 所有数据类型支持(BOOLEAN 到 STRING) |
| - ✅ BitMap 基础实现(null 标记) |
| - ✅ Big-endian 字节序(Java 兼容) |
| |
| **优化空间:** |
| |
| 1. **BitMap 打包优化:** 当前实现已正确但可添加注释说明位打包逻辑 |
| 2. **TSEncoding 压缩:** 未实现 RLE、GORILLA 等压缩算法 |
| 3. **内存分配策略:** 可预先计算总大小避免多次 Buffer.concat |
| 4. **WAL 格式支持:** 未实现写前日志序列化格式 |
| |
| ### 4.2 优化实施步骤 |
| |
| #### Step 1: 添加 TSEncoding 支持 |
| |
| **文件:** `src/utils/TSEncoding.ts` (新建) |
| |
| ```typescript |
| /** |
| * TSEncoding types supported by Apache IoTDB |
| */ |
| export enum TSEncoding { |
| PLAIN = 0, // No compression |
| RLE = 2, // Run-Length Encoding |
| TS_2DIFF = 3, // Two-level delta encoding (for timestamps) |
| GORILLA = 4, // Gorilla compression (for float/double) |
| DICTIONARY = 5, // Dictionary encoding (for strings) |
| // ... other encodings |
| } |
| |
| /** |
| * Encoder interface for different compression strategies |
| */ |
| export interface ValueEncoder { |
| encode( |
| values: any[], |
| dataType: number, |
| nullIndicators: boolean[] | null, |
| ): Buffer; |
| } |
| |
| /** |
| * PLAIN encoder - no compression, just serialize values as-is |
| */ |
| export class PlainEncoder implements ValueEncoder { |
| encode( |
| values: any[], |
| dataType: number, |
| nullIndicators: boolean[] | null, |
| ): Buffer { |
| // Delegate to existing serializeColumn logic |
| // This is the default implementation |
| return serializeColumnPlain(values, dataType); |
| } |
| } |
| |
| /** |
| * RLE encoder - compresses runs of identical values |
| * Format: [count: INT32][value: TYPE-SPECIFIC] |
| */ |
| export class RleEncoder implements ValueEncoder { |
| encode( |
| values: any[], |
| dataType: number, |
| nullIndicators: boolean[] | null, |
| ): Buffer { |
| const runs: Array<{ value: any; count: number }> = []; |
| |
| // Find runs of identical values |
| let currentValue = values[0]; |
| let currentCount = 1; |
| |
| for (let i = 1; i < values.length; i++) { |
| if (this.valuesEqual(values[i], currentValue, dataType)) { |
| currentCount++; |
| } else { |
| runs.push({ value: currentValue, count: currentCount }); |
| currentValue = values[i]; |
| currentCount = 1; |
| } |
| } |
| runs.push({ value: currentValue, count: currentCount }); |
| |
| // Serialize runs |
| const buffers: Buffer[] = []; |
| |
| // Write number of runs |
| const runCountBuffer = Buffer.alloc(4); |
| runCountBuffer.writeInt32BE(runs.length); |
| buffers.push(runCountBuffer); |
| |
| // Write each run |
| for (const run of runs) { |
| // Write count |
| const countBuffer = Buffer.alloc(4); |
| countBuffer.writeInt32BE(run.count); |
| buffers.push(countBuffer); |
| |
| // Write value (single value serialization) |
| buffers.push(this.serializeSingleValue(run.value, dataType)); |
| } |
| |
| return Buffer.concat(buffers); |
| } |
| |
| private valuesEqual(a: any, b: any, dataType: number): boolean { |
| if (a === null || b === null) return a === b; |
| |
| switch (dataType) { |
| case 0: // BOOLEAN |
| case 1: // INT32 |
| case 9: // DATE |
| return a === b; |
| case 2: // INT64 |
| case 8: // TIMESTAMP |
| return BigInt(a) === BigInt(b); |
| case 3: // FLOAT |
| case 4: // DOUBLE |
| return Math.abs(a - b) < 1e-9; // Float comparison |
| case 5: // TEXT |
| case 11: // STRING |
| return String(a) === String(b); |
| case 10: // BLOB |
| return Buffer.isBuffer(a) && Buffer.isBuffer(b) && a.equals(b); |
| default: |
| return false; |
| } |
| } |
| |
| private serializeSingleValue(value: any, dataType: number): Buffer { |
| // Similar to serializeColumn but for single value |
| // ... implementation |
| } |
| } |
| |
| /** |
| * GORILLA encoder - for floating point compression |
| * Uses XOR-based compression (Facebook's Gorilla algorithm) |
| */ |
| export class GorillaEncoder implements ValueEncoder { |
| encode( |
| values: any[], |
| dataType: number, |
| nullIndicators: boolean[] | null, |
| ): Buffer { |
| if (dataType !== 3 && dataType !== 4) { |
| throw new Error("GORILLA encoding only supports FLOAT and DOUBLE"); |
| } |
| |
| // Implement Gorilla compression algorithm |
| // - Store first value as-is |
| // - For subsequent values: |
| // - XOR with previous value |
| // - If XOR = 0, write single bit 0 |
| // - If leading/trailing zeros match previous, write compact format |
| // - Otherwise, write full XOR value |
| |
| // TODO: Full Gorilla implementation |
| // This is complex, may defer to future PR |
| throw new Error("GORILLA encoding not yet implemented"); |
| } |
| } |
| ``` |
| |
| **配置支持:** |
| |
| ```typescript |
| // src/utils/Config.ts |
| export interface TabletConfig { |
| /** |
| * Encoding strategy for tablet values. |
| * @default TSEncoding.PLAIN |
| */ |
| encoding?: TSEncoding; |
| |
| /** |
| * Enable RLE compression for tablets with repeated values. |
| * Only applies if encoding=PLAIN. |
| * @default false |
| */ |
| enableAutoCompression?: boolean; |
| } |
| ``` |
| |
| #### Step 2: 优化内存分配 |
| |
| **文件:** Session.ts |
| |
| 当前实现使用多次 `Buffer.concat`,可优化为预先计算大小: |
| |
| ```typescript |
| protected serializeTabletValues( |
| values: any[][], |
| dataTypes: number[], |
| rowCount: number, |
| ): Buffer { |
| // NEW: Pre-calculate total buffer size |
| let totalSize = 0; |
| const columnBuffers: Buffer[] = []; |
| const bitMaps: (boolean[] | null)[] = []; |
| |
| // Phase 1: Serialize columns and calculate sizes |
| for (let colIndex = 0; colIndex < dataTypes.length; colIndex++) { |
| const dataType = dataTypes[colIndex]; |
| const columnValues = values.map((row) => row[colIndex]); |
| |
| // Track null values |
| const nullBitmap: boolean[] = []; |
| let hasNull = false; |
| |
| for (let rowIndex = 0; rowIndex < columnValues.length; rowIndex++) { |
| const isNull = columnValues[rowIndex] === null || columnValues[rowIndex] === undefined; |
| nullBitmap.push(isNull); |
| if (isNull) hasNull = true; |
| } |
| |
| const buffer = this.serializeColumn(columnValues, dataType); |
| columnBuffers.push(buffer); |
| bitMaps.push(hasNull ? nullBitmap : null); |
| |
| totalSize += buffer.length; |
| } |
| |
| // Phase 2: Calculate bitmap size |
| const bitmapBuffer = this.serializeBitMaps(bitMaps, rowCount); |
| totalSize += bitmapBuffer.length; |
| |
| // Phase 3: Allocate single buffer and copy |
| const result = Buffer.allocUnsafe(totalSize); |
| let offset = 0; |
| |
| for (const buffer of columnBuffers) { |
| buffer.copy(result, offset); |
| offset += buffer.length; |
| } |
| |
| bitmapBuffer.copy(result, offset); |
| |
| return result; |
| } |
| ``` |
| |
| **性能改进:** |
| |
| - 减少内存分配次数:从 O(n) 到 O(1) |
| - 避免多次 Buffer 拷贝:从 O(n²) 到 O(n) |
| - 预期性能提升:15-25%(大 tablet 场景) |
| |
| #### Step 3: 添加详细注释和文档 |
| |
| **文件:** Session.ts |
| |
| 增强 BitMap 序列化的代码注释: |
| |
| ```typescript |
| protected serializeBitMaps( |
| bitMaps: (boolean[] | null)[], |
| rowCount: number, |
| ): Buffer { |
| /** |
| * BitMap Serialization Format (matches Apache IoTDB Java client): |
| * |
| * For each column: |
| * 1. columnHasNull flag (1 byte): 0=no nulls, 1=has nulls |
| * 2. If has nulls: bitmap array |
| * - 8 values packed per byte (big-endian bit ordering) |
| * - Bit=1 means NULL, Bit=0 means NOT NULL |
| * - Size = Math.ceil(rowCount / 8) bytes |
| * - Padding: Remaining bits in last byte are set to 0 |
| * |
| * Example for rowCount=10: |
| * Row indices: 0 1 2 3 4 5 6 7 | 8 9 (padding 6 bits) |
| * Null values: 0 1 0 0 1 0 1 0 | 0 1 0 0 0 0 0 0 |
| * Packed byte: 0b01001010 | 0b01000000 |
| * Hex: 0x4A | 0x40 |
| * |
| * This matches the bit packing in: |
| * - Java: InsertTabletNode.writeBitMaps() (lines 562-581) |
| * - C++: Session::getValue() with BitMap serialization |
| * - Python: Tablet.get_binary_values() with struct.pack |
| */ |
| |
| const buffers: Buffer[] = []; |
| |
| for (const bitMap of bitMaps) { |
| const columnHasNull = bitMap !== null; |
| |
| // Write columnHasNull flag (1 byte) |
| buffers.push(Buffer.from([columnHasNull ? 1 : 0])); |
| |
| if (columnHasNull && bitMap) { |
| // Calculate bitmap byte count |
| // Example: 10 rows → Math.ceil(10/8) = 2 bytes |
| const bitmapByteCount = Math.ceil(rowCount / 8); |
| const bitmapBytes = Buffer.alloc(bitmapByteCount); |
| |
| // Pack 8 bits per byte (big-endian ordering) |
| for (let i = 0; i < bitMap.length; i++) { |
| if (bitMap[i]) { |
| const byteIndex = Math.floor(i / 8); |
| const bitIndex = i % 8; |
| |
| // Set bit from left to right (MSB first) |
| // Bit 0 → 0x80 (10000000) |
| // Bit 1 → 0x40 (01000000) |
| // Bit 2 → 0x20 (00100000) |
| // ... |
| bitmapBytes[byteIndex] |= (1 << (7 - bitIndex)); |
| } |
| } |
| |
| buffers.push(bitmapBytes); |
| } |
| } |
| |
| return Buffer.concat(buffers); |
| } |
| ``` |
| |
| **注意:** 检查当前实现的位序是否与 Java 匹配: |
| |
| - Java: `1 << bitIndex` (LSB first) |
| - 当前实现: `1 << bitIndex` (matches Java) |
| |
| ### 4.3 测试计划 |
| |
| #### Unit Tests |
| |
| **文件:** `tests/unit/TabletSerialization.test.ts` (新建) |
| |
| ```typescript |
| describe("Tablet Serialization", () => { |
| test("should serialize bitmap with correct bit packing", () => { |
| const session = new Session({ host: "localhost", port: 6667 }); |
| |
| // Test data: 10 rows, column 0 has nulls at indices 1, 4, 6, 9 |
| const bitMaps = [ |
| [false, true, false, false, true, false, true, false, false, true], // Column 0 |
| null, // Column 1: no nulls |
| ]; |
| |
| const buffer = (session as any).serializeBitMaps(bitMaps, 10); |
| |
| // Expected format: |
| // Column 0: [0x01] (has null) + [0x4A, 0x40] (bitmap) |
| // Bits: 01001010 01000000 |
| // Explanation: bit 1=1, bit 4=1, bit 6=1, bit 9=1 |
| // Column 1: [0x00] (no null) |
| |
| expect(buffer[0]).toBe(0x01); // Column 0 has null |
| expect(buffer[1]).toBe(0x4a); // First byte: 01001010 |
| expect(buffer[2]).toBe(0x40); // Second byte: 01000000 (bit 9 set) |
| expect(buffer[3]).toBe(0x00); // Column 1 no null |
| }); |
| |
| test("should handle edge case: rowCount not multiple of 8", () => { |
| // Test with 13 rows (needs 2 bytes, last 3 bits unused) |
| const bitMaps = [ |
| [ |
| true, |
| false, |
| false, |
| true, |
| false, |
| false, |
| false, |
| false, |
| true, |
| false, |
| true, |
| false, |
| false, |
| ], |
| ]; |
| const buffer = (session as any).serializeBitMaps(bitMaps, 13); |
| |
| // Expected: [0x01] + [0b10010000, 0b10100000] |
| expect(buffer[0]).toBe(0x01); |
| expect(buffer[1]).toBe(0x90); // 10010000 |
| expect(buffer[2]).toBe(0xa0); // 10100000 |
| }); |
| }); |
| ``` |
| |
| #### Performance Tests |
| |
| **文件:** `tests/performance/TabletSerialization.bench.ts` (新建) |
| |
| ```typescript |
| describe("Tablet Serialization Performance", () => { |
| test("benchmark: large tablet serialization", () => { |
| const session = new Session({ host: "localhost", port: 6667 }); |
| |
| // Create large tablet: 1000 rows x 100 columns |
| const rowCount = 1000; |
| const colCount = 100; |
| |
| const tablet = { |
| deviceId: "root.perf.device1", |
| measurements: Array.from({ length: colCount }, (_, i) => `s${i}`), |
| dataTypes: Array(colCount).fill(TSDataType.FLOAT), |
| timestamps: Array.from( |
| { length: rowCount }, |
| (_, i) => Date.now() + i * 1000, |
| ), |
| values: Array.from({ length: rowCount }, () => |
| Array.from({ length: colCount }, () => Math.random() * 100), |
| ), |
| }; |
| |
| const startTime = Date.now(); |
| const buffer = (session as any).serializeTabletValues( |
| tablet.values, |
| tablet.dataTypes, |
| rowCount, |
| ); |
| const duration = Date.now() - startTime; |
| |
| console.log(`Serialized ${rowCount}x${colCount} tablet in ${duration}ms`); |
| console.log(`Buffer size: ${buffer.length} bytes`); |
| console.log( |
| `Throughput: ${(buffer.length / duration / 1024).toFixed(2)} MB/s`, |
| ); |
| |
| // Performance target: < 50ms for 1000x100 tablet |
| expect(duration).toBeLessThan(50); |
| }); |
| }); |
| ``` |
| |
| --- |
| |
| ## 五、查询结果反序列化增强规划 |
| |
| ### 5.1 当前实现分析 |
| |
| **已实现:** |
| |
| - ✅ TsBlock 格式解析 |
| - ✅ 流式迭代器(hasNext/next) |
| - ✅ 5 种列编码:ByteArray, Int32Array, Int64Array, BinaryArray, RLE |
| - ✅ Null indicators 处理 |
| - ✅ 列名到索引映射 |
| |
| **优化空间:** |
| |
| 1. **高级编码支持:** DICTIONARY, FREQ 等编码未实现 |
| 2. **列式访问优化:** 可缓存解码后的列数据 |
| 3. **内存使用优化:** 大结果集的分批释放 |
| 4. **类型转换优化:** 减少重复的类型检查 |
| |
| ### 5.2 优化实施步骤 |
| |
| #### Step 1: 扩展 ColumnDecoder 支持更多编码 |
| |
| **文件:** ColumnDecoder.ts |
| |
| 添加 DICTIONARY 编码支持: |
| |
| ```typescript |
| /** |
| * Decoder for DICTIONARY encoding (encoding=5) |
| * Uses integer keys to reference a string dictionary. |
| * Efficient for columns with low cardinality (many repeated values). |
| * |
| * Format: |
| * [dictionarySize: INT32] |
| * [key1: String, key2: String, ..., keyN: String] |
| * [indices: INT32 array with positionCount entries] |
| * [nullIndicators: bitmap] |
| */ |
| class DictionaryColumnDecoder implements ColumnDecoder { |
| readColumn( |
| buffer: Buffer, |
| offset: number, |
| dataType: number, |
| positionCount: number, |
| ): { column: Column; bytesRead: number } { |
| if (dataType !== 5 && dataType !== 11) { |
| throw new Error("DICTIONARY encoding only supports TEXT/STRING"); |
| } |
| |
| let currentOffset = offset; |
| |
| // Read dictionary size |
| const dictionarySize = buffer.readInt32BE(currentOffset); |
| currentOffset += 4; |
| |
| // Read dictionary entries |
| const dictionary: string[] = new Array(dictionarySize); |
| for (let i = 0; i < dictionarySize; i++) { |
| const length = buffer.readInt32BE(currentOffset); |
| currentOffset += 4; |
| |
| dictionary[i] = buffer.toString( |
| "utf8", |
| currentOffset, |
| currentOffset + length, |
| ); |
| currentOffset += length; |
| } |
| |
| logger.debug(`Dictionary decoded: ${dictionarySize} unique values`); |
| |
| // Read indices array |
| const indices: number[] = new Array(positionCount); |
| for (let i = 0; i < positionCount; i++) { |
| indices[i] = buffer.readInt32BE(currentOffset); |
| currentOffset += 4; |
| } |
| |
| // Read null indicators |
| const { nullIndicators, bytesRead: nullBytes } = |
| ColumnDeserializer.deserializeNullIndicators( |
| buffer, |
| currentOffset, |
| positionCount, |
| ); |
| currentOffset += nullBytes; |
| |
| // Map indices to dictionary values |
| const values: any[] = new Array(positionCount); |
| for (let i = 0; i < positionCount; i++) { |
| if (nullIndicators && nullIndicators[i]) { |
| values[i] = null; |
| } else { |
| const dictIndex = indices[i]; |
| if (dictIndex < 0 || dictIndex >= dictionarySize) { |
| throw new Error(`Invalid dictionary index: ${dictIndex}`); |
| } |
| values[i] = dictionary[dictIndex]; |
| } |
| } |
| |
| return { |
| column: { |
| dataType, |
| encoding: ColumnEncoding.Dictionary, |
| values, |
| nullIndicators, |
| positionCount, |
| }, |
| bytesRead: currentOffset - offset, |
| }; |
| } |
| } |
| |
| // Add to BaseColumnDecoder |
| export enum ColumnEncoding { |
| ByteArray = 0, |
| Int32Array = 1, |
| Int64Array = 2, |
| BinaryArray = 3, |
| Rle = 4, |
| Dictionary = 5, // NEW |
| } |
| |
| export class BaseColumnDecoder { |
| private static decoders: Map<ColumnEncoding, ColumnDecoder> = new Map([ |
| [ColumnEncoding.Int32Array, new Int32ArrayColumnDecoder()], |
| [ColumnEncoding.Int64Array, new Int64ArrayColumnDecoder()], |
| [ColumnEncoding.ByteArray, new ByteArrayColumnDecoder()], |
| [ColumnEncoding.BinaryArray, new BinaryArrayColumnDecoder()], |
| [ColumnEncoding.Rle, new RunLengthColumnDecoder()], |
| [ColumnEncoding.Dictionary, new DictionaryColumnDecoder()], // NEW |
| ]); |
| |
| // ... rest of implementation |
| } |
| ``` |
| |
| #### Step 2: 优化 RowRecord 列访问 |
| |
| **文件:** RowRecord.ts |
| |
| 添加列值缓存和类型转换优化: |
| |
| ```typescript |
| export class RowRecord { |
| private timestamp: number; |
| private fields: any[]; |
| private columnNames: string[]; |
| private columnNameIndexMap: Map<string, number>; |
| |
| // NEW: Cache for type-converted values |
| private convertedValuesCache: Map<number, any> = new Map(); |
| |
| constructor( |
| timestamp: number, |
| fields: any[], |
| columnNames: string[], |
| columnNameIndexMap: Map<string, number>, |
| ) { |
| this.timestamp = timestamp; |
| this.fields = fields; |
| this.columnNames = columnNames; |
| this.columnNameIndexMap = columnNameIndexMap; |
| } |
| |
| /** |
| * Get FLOAT value with caching. |
| */ |
| getFloat(columnName: string): number { |
| const index = this.columnNameIndexMap.get(columnName); |
| if (index === undefined) { |
| throw new Error(`Column not found: ${columnName}`); |
| } |
| |
| // Check cache |
| const cacheKey = index * 1000 + 3; // Encode index + type |
| if (this.convertedValuesCache.has(cacheKey)) { |
| return this.convertedValuesCache.get(cacheKey); |
| } |
| |
| const value = this.fields[index]; |
| if (value === null || value === undefined) { |
| return NaN; |
| } |
| |
| const converted = Number(value); |
| this.convertedValuesCache.set(cacheKey, converted); |
| return converted; |
| } |
| |
| // Similar optimizations for getInt, getDouble, etc. |
| } |
| ``` |
| |
| #### Step 3: 添加列式批量访问 |
| |
| **文件:** SessionDataSet.ts |
| |
| 新增批量列访问方法以提升大查询性能: |
| |
| ```typescript |
| export class SessionDataSet { |
| // ... existing fields ... |
| |
| /** |
| * Get entire column as array (efficient for columnar operations). |
| * Only fetches data up to current position. |
| * |
| * @param columnName Column name |
| * @returns Array of column values (may include nulls) |
| */ |
| getColumnArray(columnName: string): any[] { |
| const columnIndex = this.columnNameIndexMap.get(columnName); |
| if (columnIndex === undefined) { |
| throw new Error(`Column not found: ${columnName}`); |
| } |
| |
| // Extract column from all cached rows |
| return this.currentRows.map((row) => { |
| // Account for timestamp column if present |
| const dataColumnIndex = this.ignoreTimeStamp |
| ? columnIndex |
| : columnIndex + 1; |
| return row[dataColumnIndex]; |
| }); |
| } |
| |
| /** |
| * Get multiple columns as arrays in single call. |
| * More efficient than multiple getColumnArray calls. |
| * |
| * @param columnNames Array of column names |
| * @returns Map of column name to value array |
| */ |
| getColumnsAsArrays(columnNames: string[]): Map<string, any[]> { |
| const result = new Map<string, any[]>(); |
| |
| for (const columnName of columnNames) { |
| result.set(columnName, this.getColumnArray(columnName)); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Convert current batch to columnar format (for analytics). |
| * Returns { timestamp: number[], column1: any[], column2: any[], ... } |
| */ |
| toColumnarBatch(): Record<string, any[]> { |
| const result: Record<string, any[]> = {}; |
| |
| // Extract timestamps if present |
| if (!this.ignoreTimeStamp) { |
| result.timestamp = this.currentRows.map((row) => row[0]); |
| } |
| |
| // Extract each column |
| for (let i = 0; i < this.columnNames.length; i++) { |
| const columnName = this.columnNames[i]; |
| const columnIndex = this.ignoreTimeStamp ? i : i + 1; |
| result[columnName] = this.currentRows.map((row) => row[columnIndex]); |
| } |
| |
| return result; |
| } |
| } |
| ``` |
| |
| ### 5.3 测试计划 |
| |
| #### Unit Tests |
| |
| **文件:** `tests/unit/ColumnDecoder.test.ts` (扩展) |
| |
| ```typescript |
| describe("ColumnDecoder - DICTIONARY encoding", () => { |
| test("should decode dictionary-encoded column", () => { |
| // Create test buffer with dictionary encoding |
| const buffer = Buffer.alloc(1000); |
| let offset = 0; |
| |
| // Dictionary size: 3 |
| buffer.writeInt32BE(3, offset); |
| offset += 4; |
| |
| // Dictionary entries: "RED", "GREEN", "BLUE" |
| const entries = ["RED", "GREEN", "BLUE"]; |
| for (const entry of entries) { |
| const entryBuffer = Buffer.from(entry, "utf8"); |
| buffer.writeInt32BE(entryBuffer.length, offset); |
| offset += 4; |
| entryBuffer.copy(buffer, offset); |
| offset += entryBuffer.length; |
| } |
| |
| // Indices: [0, 1, 2, 0, 1, 0] (6 values) |
| const indices = [0, 1, 2, 0, 1, 0]; |
| for (const index of indices) { |
| buffer.writeInt32BE(index, offset); |
| offset += 4; |
| } |
| |
| // No nulls |
| buffer.writeUInt8(0, offset); |
| offset += 1; |
| |
| // Decode |
| const decoder = BaseColumnDecoder.getDecoder(ColumnEncoding.Dictionary); |
| const { column } = decoder.readColumn(buffer, 0, TSDataType.TEXT, 6); |
| |
| expect(column.values).toEqual([ |
| "RED", |
| "GREEN", |
| "BLUE", |
| "RED", |
| "GREEN", |
| "RED", |
| ]); |
| }); |
| }); |
| ``` |
| |
| #### Performance Tests |
| |
| **文件:** `tests/performance/QueryDeserialization.bench.ts` (新建) |
| |
| ```typescript |
| describe("Query Deserialization Performance", () => { |
| test("benchmark: large result set iteration", async () => { |
| const session = new Session({ |
| host: process.env.IOTDB_HOST || "localhost", |
| port: parseInt(process.env.IOTDB_PORT || "6667"), |
| fetchSize: 10000, // Large fetch size |
| }); |
| |
| await session.open(); |
| |
| // Query large dataset |
| const startTime = Date.now(); |
| const dataSet = await session.executeQueryStatement( |
| "SELECT * FROM root.test.** LIMIT 100000", |
| ); |
| |
| let rowCount = 0; |
| let totalDeserializeTime = 0; |
| |
| while (await dataSet.hasNext()) { |
| const batchStart = Date.now(); |
| const row = dataSet.next(); |
| totalDeserializeTime += Date.now() - batchStart; |
| rowCount++; |
| } |
| |
| await dataSet.close(); |
| await session.close(); |
| |
| const totalTime = Date.now() - startTime; |
| |
| console.log(`Deserialized ${rowCount} rows in ${totalTime}ms`); |
| console.log( |
| `Average per row: ${(totalDeserializeTime / rowCount).toFixed(3)}ms`, |
| ); |
| console.log( |
| `Throughput: ${(rowCount / (totalTime / 1000)).toFixed(0)} rows/sec`, |
| ); |
| |
| // Performance target: > 50,000 rows/sec |
| expect(rowCount / (totalTime / 1000)).toBeGreaterThan(50000); |
| }); |
| |
| test("benchmark: columnar access vs row-by-row", async () => { |
| const dataSet = await session.executeQueryStatement( |
| "SELECT * FROM root.test.** LIMIT 10000", |
| ); |
| |
| // Method 1: Row-by-row access |
| const rowStartTime = Date.now(); |
| const rowResults = []; |
| while (await dataSet.hasNext()) { |
| const row = dataSet.next(); |
| rowResults.push(row.getValue("temperature")); |
| } |
| const rowTime = Date.now() - rowStartTime; |
| |
| // Method 2: Columnar access |
| const colStartTime = Date.now(); |
| const colResults = dataSet.getColumnArray("temperature"); |
| const colTime = Date.now() - colStartTime; |
| |
| console.log(`Row-by-row: ${rowTime}ms, Columnar: ${colTime}ms`); |
| console.log(`Speedup: ${(rowTime / colTime).toFixed(2)}x`); |
| |
| // Columnar access should be faster |
| expect(colTime).toBeLessThan(rowTime); |
| }); |
| }); |
| ``` |
| |
| --- |
| |
| ## 六、实施优先级和时间表 |
| |
| ### 6.1 优先级排序 |
| |
| | 功能 | 优先级 | 预计工作量 | 性能影响 | 用户影响 | |
| | --------------------- | ------------- | ---------- | -------------- | -------- | |
| | **Redirection 支持** | **P0 (最高)** | 5-7 天 | +++++ (40-60%) | High | |
| | **Tablet 序列化优化** | **P1 (高)** | 3-4 天 | +++ (15-25%) | Medium | |
| | **查询反序列化增强** | **P2 (中)** | 4-5 天 | ++ (10-20%) | Medium | |
| |
| **优先级理由:** |
| |
| 1. **Redirection** 是写入性能的核心优化,影响最大 |
| 2. **Tablet 序列化** 是基础设施改进,影响所有写入操作 |
| 3. **查询反序列化** 主要优化读取场景,相对影响较小 |
| |
| ### 6.2 实施阶段 |
| |
| #### Phase 1: Redirection 核心功能 (Week 1-2) |
| |
| - [ ] Step 1: 扩展 Config 接口 (1 天) |
| - [ ] Step 2: 实现 RedirectException 和 RedirectCache (2 天) |
| - [ ] Step 3: 增强 BaseSessionPool (2 天) |
| - [ ] Step 4: 更新 Session.insertTablet (1 天) |
| - [ ] Step 5: 单元测试和E2E测试 (1-2 天) |
| |
| #### Phase 2: Tablet 序列化优化 (Week 3) |
| |
| - [ ] Step 1: 添加 TSEncoding 基础框架 (1 天) |
| - [ ] Step 2: 实现 PLAIN 和 RLE 编码器 (1 天) |
| - [ ] Step 3: 优化内存分配策略 (1 天) |
| - [ ] Step 4: 增强代码注释和文档 (0.5 天) |
| - [ ] Step 5: 性能测试和基准 (0.5 天) |
| |
| #### Phase 3: 查询反序列化增强 (Week 4) |
| |
| - [ ] Step 1: 实现 DICTIONARY 编码器 (1 天) |
| - [ ] Step 2: 优化 RowRecord 列访问 (1 天) |
| - [ ] Step 3: 添加列式批量访问 API (1 天) |
| - [ ] Step 4: 单元测试和性能测试 (1 天) |
| - [ ] Step 5: 文档更新和示例 (1 天) |
| |
| ### 6.3 里程碑和交付物 |
| |
| **Milestone 1 (Week 2):** Redirection 支持完成 |
| |
| - ✅ 功能代码实现 |
| - ✅ 单元测试 (>90% 覆盖率) |
| - ✅ E2E 测试(真实 IoTDB 集群) |
| - ✅ 性能基准测试报告 |
| - ✅ 用户文档和示例 |
| |
| **Milestone 2 (Week 3):** Tablet 序列化优化完成 |
| |
| - ✅ TSEncoding 框架实现 |
| - ✅ 内存优化实施 |
| - ✅ 性能对比报告 |
| - ✅ 代码注释和文档 |
| |
| **Milestone 3 (Week 4):** 查询反序列化增强完成 |
| |
| - ✅ 高级编码支持 |
| - ✅ 列式访问 API |
| - ✅ 性能测试报告 |
| - ✅ API 文档和示例 |
| |
| --- |
| |
| ## 七、风险和依赖 |
| |
| ### 7.1 技术风险 |
| |
| | 风险 | 可能性 | 影响 | 缓解措施 | |
| | ------------------------ | ------ | ---- | -------------------------------- | |
| | **Redirection 协议变更** | 低 | 高 | 参考最新 Java 源码,保持同步更新 | |
| | **编码实现不兼容** | 中 | 中 | 充分测试 Java 互操作性 | |
| | **性能回归** | 低 | 高 | 每个 PR 运行性能基准测试 | |
| | **内存泄漏** | 中 | 高 | 压力测试和内存分析 | |
| |
| ### 7.2 依赖项 |
| |
| **外部依赖:** |
| |
| - Apache IoTDB v1.0+ (服务端支持 Redirection) |
| - Thrift v0.22.0 (协议兼容性) |
| |
| **内部依赖:** |
| |
| - 当前 Session/SessionPool 实现稳定 |
| - ColumnDecoder 框架完整 |
| - 测试基础设施就绪 |
| |
| ### 7.3 测试策略 |
| |
| **单元测试:** |
| |
| - RedirectCache 逻辑测试 |
| - TSEncoding 编码器测试 |
| - ColumnDecoder 解码器测试 |
| - BitMap 序列化测试 |
| |
| **集成测试:** |
| |
| - Redirection 端到端流程 |
| - 多节点负载均衡 |
| - 编码兼容性测试 |
| |
| **性能测试:** |
| |
| - 写入吞吐量测试 |
| - 查询延迟测试 |
| - 内存使用测试 |
| - 并发压力测试 |
| |
| **兼容性测试:** |
| |
| - 与 Java client 数据交换 |
| - 与 C++ client 数据交换 |
| - 与 Python client 数据交换 |
| |
| --- |
| |
| ## 八、成功标准 |
| |
| ### 8.1 功能标准 |
| |
| **Redirection:** |
| |
| - ✅ 支持 RedirectException 处理 |
| - ✅ 设备到端点映射缓存生效 |
| - ✅ 故障转移和重试机制正常 |
| - ✅ 配置开关可控 |
| |
| **Tablet 序列化:** |
| |
| - ✅ PLAIN/RLE 编码正常工作 |
| - ✅ BitMap 序列化符合协议 |
| - ✅ 内存分配优化生效 |
| - ✅ 与 Java 客户端互操作 |
| |
| **查询反序列化:** |
| |
| - ✅ DICTIONARY 编码支持 |
| - ✅ 列式访问 API 可用 |
| - ✅ 大结果集流式处理 |
| - ✅ 类型转换优化生效 |
| |
| ### 8.2 性能标准 |
| |
| **Redirection:** |
| |
| - 写入延迟降低 30-40% |
| - 吞吐量提升 40-60% |
| - 缓存命中率 >80% |
| |
| **Tablet 序列化:** |
| |
| - 序列化时间降低 15-25% |
| - 内存分配次数减少 >50% |
| - Buffer 拷贝次数减少 >70% |
| |
| **查询反序列化:** |
| |
| - 反序列化速度提升 10-20% |
| - 列式访问比行式快 2-5x |
| - 内存峰值降低 20-30% |
| |
| ### 8.3 质量标准 |
| |
| - 单元测试覆盖率 >90% |
| - E2E 测试通过率 100% |
| - 性能测试无回归 |
| - 代码审查通过 |
| - 文档完整准确 |
| |
| --- |
| |
| ## 九、参考资料 |
| |
| ### 9.1 源码参考 |
| |
| **Java 实现:** |
| |
| - [Session.java](https://github.com/apache/iotdb/blob/master/iotdb-client/session/src/main/java/org/apache/iotdb/session/Session.java) - Redirection 核心实现 |
| - [InsertTabletNode.java](https://github.com/apache/iotdb/blob/master/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/write/InsertTabletNode.java) - Tablet 序列化 |
| - [QueryDataSetUtils.java](https://github.com/apache/iotdb/blob/master/iotdb-client/session/src/main/java/org/apache/iotdb/session/QueryDataSetUtils.java) - 查询结果反序列化 |
| - [IoTDBRpcDataSet.java](https://github.com/apache/iotdb/blob/master/iotdb-client/session/src/main/java/org/apache/iotdb/session/IoTDBRpcDataSet.java) - 流式结果集 |
| |
| **C++ 实现:** |
| |
| - [Session.cpp](https://github.com/apache/iotdb/blob/master/iotdb-client/client-cpp/src/main/Session.cpp) - Redirection 和序列化 |
| - [TsBlock.cpp](https://github.com/apache/iotdb/blob/master/iotdb-client/client-cpp/src/main/TsBlock.cpp) - TsBlock 反序列化 |
| |
| **Python 实现:** |
| |
| - [Session.py](https://github.com/apache/iotdb/blob/master/iotdb-client/client-py/iotdb/Session.py) - Redirection 处理 |
| - [tsblock_serde.py](https://github.com/apache/iotdb/blob/master/iotdb-client/client-py/iotdb/tsblock_serde.py) - TsBlock 反序列化 |
| |
| ### 9.2 协议文档 |
| |
| - [Apache IoTDB Thrift Protocol](https://github.com/apache/iotdb/tree/master/iotdb-protocol) |
| - [TSFile Format Specification](https://iotdb.apache.org/UserGuide/Master/Technical-Insider/TsFile.html) |
| - [Binary Protocol Documentation](https://thrift.apache.org/docs/types) |
| |
| ### 9.3 性能优化资源 |
| |
| - [Java Performance Tuning Guide](https://iotdb.apache.org/UserGuide/Master/Performance/Performance-Tuning.html) |
| - [Node.js Buffer Performance](https://nodejs.org/api/buffer.html#buffer-performance) |
| - [Gorilla Compression Algorithm](https://www.vldb.org/pvldb/vol8/p1816-teller.pdf) |
| |
| --- |
| |
| ## 十、总结 |
| |
| 本规划文档详细分析了 Apache IoTDB Node.js 客户端在以下三个核心技术领域的现状、差距和实施路径: |
| |
| 1. **完整的 Redirection 支持**:通过实现 RedirectCache、RedirectException 处理和智能连接路由,预期将写入性能提升 40-60% |
| |
| 2. **Tablet 序列化优化**:通过引入 TSEncoding 压缩、优化内存分配和增强 BitMap 处理,预期将序列化性能提升 15-25% |
| |
| 3. **查询结果反序列化增强**:通过扩展列编码支持、添加列式访问 API 和优化类型转换,预期将查询性能提升 10-20% |
| |
| 按照 4 周的实施时间表,将分阶段交付这些核心功能,显著提升 Node.js 客户端在生产环境中的性能和可用性。 |