**Describe the solution you'd like**

We already can run clickhouse-server without configured ZooKeeper (the tables will start in read-only mode)
and with configured ZooKeeper, if it starts to be inaccessible at runtime (the tables will be read-only until the session is re-established).

Let's also make it possible to start up when ZooKeeper is unavailable.

**Additional context**

If that will be very hard to implement, we can consider other options:
- lazy attaching of the tables after sever start-up;
- a maintenance mode when the tables are not attached at start-up and require manual ATTACH queries.
size_t quorum,
        zkutil::ZooKeeper::Ptr zookeeper_ = nullptr,
        bool try_fetch_shared = true);
    /** Download the specified part from the specified replica.
      * Used for replace local part on the same s3-shared part in hybrid storage.
      * Returns false if part is already fetching right now.
      */
    DataPartStoragePtr fetchExistsPart(
        const String & part_name,
        const StorageMetadataPtr & metadata_snapshot,
        const String & replica_path,
        DiskPtr replaced_disk,
        String replaced_part_path);
    
    std::unordered_set<String> currently_fetching_parts;
    std::mutex currently_fetching_parts_mutex;
    
    void updateQuorum(const String & part_name, bool is_parallel);
    
    void cleanLastPartNode(const String & partition_id);
    
    bool partIsLastQuorumPart(const MergeTreePartInfo & part_info) const;
    
    bool partIsInsertingWithParallelQuorum(const MergeTreePartInfo & part_info) const;
    
    
    
    std::optional<EphemeralLockInZooKeeper> allocateBlockNumber(
        const String & partition_id, const zkutil::ZooKeeperPtr & zookeeper,
        const String & zookeeper_block_id_path = "", const String & zookeeper_path_prefix = "") const;